Skip to content

Use esbuild to transpile typescript in .svelte templates #177

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

Closed
martaver opened this issue Jun 10, 2020 · 18 comments
Closed

Use esbuild to transpile typescript in .svelte templates #177

martaver opened this issue Jun 10, 2020 · 18 comments
Assignees
Labels
enhancement New feature or request

Comments

@martaver
Copy link

Is your feature request related to a problem? Please describe.
Typescript transpilation gets slow, quickly.

Describe the solution you'd like
esbuild is currently the fastest way to transform typescript into javascript. It's a few orders of magnitude faster than tsc or babel. Using esbuild would help keep the developer experience snappy and not punish developers for choosing typescript.

Describe alternatives you've considered
Snowpack looks promising, and uses esbuild to transform ts/tsx, but their svelte setup doesn't support typescript without using svelte-preprocess. That means we're back to running things through tsc or babel, and affects some of the dev experience.

How important is this feature to you?
Anything that makes developer feedback fast is extremely valuable to me, and to everyone I imagine.

@kaisermann
Copy link
Member

As stated in #178, I'm interested in removing the type-checking process whatsoever in v4. Using esbuild is interesting, but how would someone choose to use esbuild instead of the tsc? With scss we have an implementation prop that defines which implementation should be used: sass or node-sass, but their essential APIs are identical. esbuild and tsc have differente APIs so we would need a different way of expressing that.

@kaisermann kaisermann self-assigned this Jun 10, 2020
@kaisermann kaisermann added the enhancement New feature or request label Jun 10, 2020
@kaisermann kaisermann changed the title use esbuild to transpile typescript in .svelte templates Use esbuild to transpile typescript in .svelte templates Jun 10, 2020
@martaver
Copy link
Author

I'm guessing it's not as simple as transpile(typescript, tsconfig): javascript...? :P

@dominikg
Copy link
Member

While you could do one preprocessor that used either tsc or esbuild depending on a preference, i don't think that would be a good solution.
Quick idea:
Rename current typescript preprocessor to typescript-tsc, add typescript-esbuild. Use the original typescript option as a delegator. (true, 'tsc' -> typescript-tsc. 'esbuild' -> typescript-esbuild.

@martaver
Copy link
Author

This issue shouldn't fade into oblivion... transpilation with esbuild vs tsc is night and day!

@kaisermann
Copy link
Member

kaisermann commented Jun 29, 2020

@martaver I didn't forget about this one! However, I'm not exactly sure what the API to change between tsc and esbuild would be.

For now, I think you can override the typescript transformer and call esbuild from there:

const preprocess = require('svelte-preprocess')
const { transformSync } = require('esbuild');

{
    ...,
    preprocess: preprocess({
      typescript({ content, filename }) {
        const { js: code } = transformSync(content, { loader: 'ts' });
        return { code };
      },
    }),
    ...
}

Also, do esbuild support the tsconfig.json? I couldn't find any documentation regarding both of them working together.

@dominikg
Copy link
Member

https://github.com/evanw/esbuild#currently-supported

Automatic detection of baseUrl and paths in tsconfig.json

so it does read tsconfig.json

this might be a good idea:
https://github.com/evanw/esbuild/blob/master/docs/js-api.md#use-a-service-for-optimal-performance

Some more information about options here (could prove useful for sourcemap support)
https://github.com/evanw/esbuild/blob/master/lib/api-types.ts

@martaver
Copy link
Author

Yeap, what he said... in addition, it's worth noting that esbuilt just strips so much of the configuration in tsconfig is superfluous. esbuild only seems to give a damn about the options specifically related to forming output with a specific js target.

@martaver
Copy link
Author

martaver commented Jun 30, 2020

Thanks for the tip with the preprocessor - works a treat!

Any thoughts on using esbuild in sapper...? Would esbuild make or break it?

There are rollup plugins that allow for using esbuild in place of babel/tsc, e.g. https://www.npmjs.com/package/rollup-plugin-esbuild.

Personally I wonder if using something like Snowpack together with Sapper would work...

@kaisermann
Copy link
Member

kaisermann commented Jul 1, 2020

@martaver The equivalent of that rollup plugin would be a svelte-preprocess-esbuild 😆. As the current solution is just five lines of code, I'm inclined to close this for now. However, if more people show interest in this, we can revive it 😁

@codechips
Copy link

A follow-up question to the people involved in the issue. Maybe you have some tips.

I used the suggested solution and it kind of works. Let me explain.

There are two ways to preprocess Typescript templates.

Standard one, using svelte-preprocess (npm add -D typescript)

// svelte.config.js

const autoPreprocess = require('svelte-preprocess');

module.exports = {
  preprocess: autoPreprocess(),
};

And faster one, using esbuild (npm add -D esbuild).

// svelte.config.js

const autoPreprocess = require('svelte-preprocess');
const { transformSync } = require('esbuild');

module.exports = {
  preprocess: autoPreprocess({
    typescript({ content, filename }) {
      const { js: code } = transformSync(content, { loader: 'ts' });
      return { code };
    },
  }),
};

Example Code

I have an external file store.ts

export const foo = (s: string) => s.split('').reverse().join('')

and App.svelte

<script type="ts">
  import { foo } from './store';

  const message: string = 'Learn Svelte';
</script>

<p>{message}</p>

<pre>{foo('dallas')}</pre>

The Problem

When using the standard preprocess everything works fine, but when using the esbuild way it fails with:

Uncaught ReferenceError: foo is not defined

The editor also gives me an error svelte(missing-declaration).

However, it works when I add a console.log(foo) somewhere in the script tag. So it looks like esbuild removes the unused declarations even if they are used in template.

Do you have any suggestions on how to solve this?

I should also add that I am using Snowpack as my bundler.

@tomocrafter
Copy link

@martaver I didn't forget about this one! However, I'm not exactly sure what the API to change between tsc and esbuild would be.

For now, I think you can override the typescript transformer and call esbuild from there:

const preprocess = require('svelte-preprocess')
const { transformSync } = require('esbuild');

{
    ...,
    preprocess: preprocess({
      typescript({ content, filename }) {
        const { js: code } = transformSync(content, { loader: 'ts' });
        return { code };
      },
    }),
    ...
}

Also, do esbuild support the tsconfig.json? I couldn't find any documentation regarding both of them working together.

It worked as I expected, thank you!
but seems like my code doesn't compile components/ (or seems like it doesn't follow import in svelte).
So it outputs

/app # yarn dev
yarn run v1.22.4
$ sapper dev
• client
/app/src/node_modules/@sapper/internal/App.svelte
'Layout' is not defined
13: </script>
14:
15: <Layout segment="{segments[0]}" {...level0.props}>
    ^
16:   {#if error}
17:     <Error {error} {status}/>
/app/src/node_modules/@sapper/internal/App.svelte
'Error' is not defined
15: <Layout segment="{segments[0]}" {...level0.props}>
16:   {#if error}
17:     <Error {error} {status}/>
        ^
18:   {:else}
19:     <svelte:component this="{level1.component}" {...level1.props}/>
/app/src/routes/_layout.svelte
'Nav' is not defined
11: }</style>
12:
13: <Nav {segment}/>
    ^
14:
15: <main>
• server
/app/src/node_modules/@sapper/internal/App.svelte
'Layout' is not defined
13: </script>
14:
15: <Layout segment="{segments[0]}" {...level0.props}>
    ^
16:   {#if error}
17:     <Error {error} {status}/>
/app/src/node_modules/@sapper/internal/App.svelte
'Error' is not defined
15: <Layout segment="{segments[0]}" {...level0.props}>
16:   {#if error}
17:     <Error {error} {status}/>
        ^
18:   {:else}
19:     <svelte:component this="{level1.component}" {...level1.props}/>
/app/src/routes/_layout.svelte
'Nav' is not defined
11: }</style>
12:
13: <Nav {segment}/>
    ^
14:
15: <main>
✔ service worker (308ms)
> Listening on http://localhost:3000

Is there any way to make it follows import?

@kaisermann
Copy link
Member

Hey @tomocrafter 👋 Did you add a typescript plugin to your bundler? rollup/webpack needs to know how to handle that TypeScript import. The preprocessor doesn't handle imports, it just transpiles your code.

@tomocrafter
Copy link

tomocrafter commented Jul 18, 2020

yes @kaisermann. I tried both rollup-plugin-typescript2 and @rollup/plugin-typescript.
(I don't know which should I use, and what is different between those.)
Here is rollup.config.js and svelte.config.js

// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import commonjs from '@rollup/plugin-commonjs';
import svelte from 'rollup-plugin-svelte';
import babel from '@rollup/plugin-babel';
import { terser } from 'rollup-plugin-terser';
import config from 'sapper/config/rollup.js';

import typescript from 'rollup-plugin-typescript2';
import svelteConfig from './svelte.config.js';

import pkg from './package.json';

const mode = process.env.NODE_ENV;
const dev = mode === 'development';
const legacy = !!process.env.SAPPER_LEGACY_BUILD;

const onwarn = (warning, onwarn) => (warning.code === 'CIRCULAR_DEPENDENCY' && /[/\\]@sapper[/\\]/.test(warning.message)) || onwarn(warning);

export default {
	client: {
		input: config.client.input().replace(/\.js$/, '.ts'),
		output: config.client.output(),
		plugins: [
			replace({
				'process.browser': true,
				'process.env.NODE_ENV': JSON.stringify(mode)
			}),
			svelte({
				dev,
				hydratable: true,
				emitCss: true,
				preprocess: svelteConfig.preprocess,
			}),
			resolve({
				browser: true,
				dedupe: ['svelte']
			}),
			commonjs(),
			typescript(),

			legacy && babel({
				extensions: ['.js', '.mjs', '.html', '.svelte'],
				babelHelpers: 'runtime',
				exclude: ['node_modules/@babel/**'],
				presets: [
					['@babel/preset-env', {
						targets: '> 0.25%, not dead'
					}]
				],
				plugins: [
					'@babel/plugin-syntax-dynamic-import',
					['@babel/plugin-transform-runtime', {
						useESModules: true
					}]
				]
			}),

			!dev && terser({
				module: true
			})
		],

		preserveEntrySignatures: false,
		onwarn,
	},

	server: {
		input: config.server.input().server.replace(/\.js$/, '.ts'),
		output: config.server.output(),
		plugins: [
			replace({
				'process.browser': false,
				'process.env.NODE_ENV': JSON.stringify(mode)
			}),
			svelte({
				generate: 'ssr',
				dev,
				preprocess: svelteConfig.preprocess,
			}),
			resolve({
				dedupe: ['svelte']
			}),
			commonjs(),
			typescript(),
		],
		external: Object.keys(pkg.dependencies).concat(
			require('module').builtinModules || Object.keys(process.binding('natives'))
		),

		preserveEntrySignatures: 'strict',
		onwarn,
	},

	serviceworker: {
		input: config.serviceworker.input(),
		output: config.serviceworker.output(),
		plugins: [
			resolve(),
			replace({
				'process.browser': true,
				'process.env.NODE_ENV': JSON.stringify(mode)
			}),
			commonjs(),
			!dev && terser()
		],

		preserveEntrySignatures: false,
		onwarn,
	}
};
//svelte.config.js
import autoPreprocess from "svelte-preprocess";
import { transformSync } from 'esbuild';

export default {
	preprocess: autoPreprocess({
		defaults: {
			script: 'typescript',
			style: 'scss'
		},
		typescript({ content }) {
			const { js: code } = transformSync(content, {
				loader: 'ts'
			});

			return { code };
		},
		//typescript: true,
		scss: true,
	}),
	// ...other svelte options
};

@codechips
Copy link

This is an esbuild issue. Looks like it removes all unused imports and since it only transpiles the script tag it doesn't see the imports used in templates.

image

@codeitlikemiley
Copy link

any news on this one? @codechips ive been reading your blog about js bundler...

can you share your best set up for svelte starter that uses typescript....

svite? esbuild?

i know svite is a plugin for vite, and vite makes the dev process fast on cold start... and uses parcel on bundling for production...

@codechips
Copy link

I would say that Svite is my favorite Svelte bundler an the moment. It has support for Typescript out of the box. I think it uses Rollup for bundling, not Parcel. Svite also comes with a few sweet templates that you can use.

You can read my "reveiw" of Svite here https://codechips.me/svelte-postcss-and-typescript-with-svite/

Also, I've been playing with SWC compiler - https://swc.rs. You can use it in Rollup instead of esbuild to transpile Typescript in Svelte files using svelte-preprocess.


import { transformSync } from '@swc/core';

// change the preprocess to this

preprocess: preprocess({
	typescript({ content }) {
		// use SWC to transpile TS scripts in Svelte files
		const { code } = transformSync(content, {
			jsc: {
				parser: { syntax: 'typescript' }
			}
		});
		return { code };
	}
})

You can also use https://github.com/mentaljam/rollup-plugin-swc to transpile regular Typescript files in Rollup to speed up compilation.

@dominikg
Copy link
Member

thanks @codechips glad you like svite.

regarding performance improvements by using esbuild over tsc in svite:
I've come to the conclusion that it's not worth the effort for now.

(to quote myself from routify discord bundler-tech channel a while ago )

long story short, in that benchmark (where the typescript compiler doesn't have much to do apart from stripping ':string' typings) it is just as fast. within margin of error
I also converted that big project from @rixo to ts. Some manual tests show around 10% overhead
first load on dev typescript: 25sec, js: 22sec. dev hmr reloads and regular refreshes are the same, thanks to svite caching :D. on build, ts build takes 15s, js build 14s ... not too bad considering it is not using esbuild for the typescript components. sure these are synthetic benchmarks with a very low load on tsc (no complicated imports, type constraints whatnot), but as of now i'm moving that esbuild-typescript preprocessor down on the priority list. speed gains would not really be noticable by the user

If you want to play with the benchmark yourself: dominikg/svite#6 . Add --ts to enable typescript

@ryanatkn
Copy link

ryanatkn commented Sep 20, 2020

I've been using swc successfully and my benchmarks show big speed boosts over ts.transpileModule, ~100x parallel on my machine, ~20x with the sync version. (for Svelte files that's only roughly 1/2 to 1/4 of the total compilation time, however, so it's not nearly as dramatic as regular ts files)

update: see svelte-preprocess-esbuild which solves the imports issue here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

7 participants