sveltejs/svelte

add documentation on "compiling" Svelte components to web components

mvolkmann opened this issue · 26 comments

I don't see a description of how to do this at the svelte.dev site and I haven't been able to find an example of this online. It would be good to see the steps required to build custom elements and then use them in a few other frameworks like React and Vue.

I did a couple of videos on using Svelte in this way... on v2, and using Angular for the host app.

https://www.youtube.com/watch?v=dZB2U83iDvg

https://www.youtube.com/watch?v=BzZ_OWqk5UE

Short version of how to produce a custom element is here in the docs:

https://svelte.dev/docs#Custom_element_API

Like the docs @kylecordes linked say, you can compile custom elements by setting an option for the compiler.

The only problem with that link is that it says to set customElements to true, but it should be customElement without the s.

@mvolkmann All you need to do is set the customElement option for the Svelte loader to true, then Svelte will compile JS files that contain custom elements (web components) which you can then import as you like.

As the docs say though, you should also either use the <svelte:options tag="my-element"> tag inside your Svelte component to define a tag name for it, or use:

import MyElement from './MyElement.svelte';

customElements.define('my-element', MyElement);

when you import it.

Thanks Jake! After configuring everything correctly, do I run "npm run build" to generate the web component definition code? Is just written to public/bundle.js? It seems like that file would contain an entire Svelte application, not just web component definitions.

If everything is part of the same application then you can just use import statements to get only the component and the fact it's all in a single bundle shouldn't make any difference.
If bundle size is an issue for you then you can just use dynamic imports and your build tool should do the code splitting for you.

If however you do actually want individual standalone files that are not part of your application and contain a single component each, then you need to compile them individually setting the component file itself as the entry point.

I don't know about Rollup, but in Webpack you can just write a loop to generate a new config for each custom component you want to compile and then import/download those files when you need them.

When you say "everything is part of the same application", are you referring to using custom elements inside a Svelte application? I'm looking for the steps to use a Svelte component as a web component in a non-Svelte application. That's the only reason I want to "compile" the Svelte components to a .js file that I can import into a non-Svelte application.

I think I'm just tripping up on the exact steps to get all of this to work. I'll throw out a challenge. Find any web page on the internet that lists the steps in terms of the actual code and commands to run. I have spent hours searching for such a site. That's why I created this issue. I assert that no such page exists. ;-)

You can import .svelte files in any type of application so long as your build tool is setup to deal with the .svelte extension correctly.

If you're using Webpack, for example, then you set up a rule to trigger when it sees the .svelte extension to compile using the svelte-loader.

In the svelte-loader you just make sure to set customElement to true, and the compiled output will be a normal JS custom element.

The fact you're using the Svelte compiler for some files inside of a mostly React project shouldn't make any difference, since the final compiled output is normal JS anyway, even if you import a .svelte file.

I’m under the impression that customElement is a configuration option for Rollup, but not for Webpack. Are you sure that’s a Webpack option?

Doesn’t it seem like there should be a way to produce a .js file as a build step that reads a set of Svelte components and outputs web component definitions ... then be able to include that .js in any web app and not even be aware that the web components were defined using Svelte? Maybe that’s something we will be able to do in the future, but not now. This is what I imagine when I hear someone like Rich Harris say “compile to web components”.

Yes customElement is definitely available in the svelte-loader since it's just a compiler option.

And if you want individual standalone JS files then you need to build the Svelte components separately from your main project.

All you need to do with Webpack is setup a config that uses your .svelte component as the entry point, set customElement in the svelte-loader to true, make sure you've defined a tag name for your component, and then tell Webpack where to output the final JS file.

You can then use that JS file in any project you like without anyone ever knowing it came from Svelte.

Thanks @jakelucas! I'll try to get that approach to work and will close this issue if I can.

@jakelucas I gave it a shot here: https://github.com/mvolkmann/svelte-in-react/tree/master/svelte-components. I have one very simple Svelte component in the file Greet.svelte. Check out my webpack.config.js file. I run it using the bundle npm script in package.json by entering npm run bundle. This gives the following error:

ERROR in ./Greet.svelte
Module parse failed: /Users/Mark/Documents/programming/languages/javascript/svelte/svelte-in-react/svelte-components/Greet.svelte Unexpected token (1:0)
You may need an appropriate loader to handle this file type.

To me it looks like I have correctly configured use of svelte-loader.
Do you see what I'm doing wrong?

@mvolkmann It works fine for me.

Your package.json didn't have any dependencies in it for the svelte-components folder, so I installed webpack, webpack-cli, and svelte-loader and it all built without problem.

I'm guessing you just don't have svelte-loader installed in that project folder.

Thanks @jakelucas! So close now. I think there's just one more thing I want to figure out.

It works if I tell Webpack to compile just one Svelte component. But I'd like to have it compile a set of components to custom elements, all within a single .js output file. I thought maybe all I would have to do is import each component into index.js or index.svelte and tell Webpack that was my entry. But neither of those worked. Do you know if it is possible to do this? See https://github.com/mvolkmann/svelte-in-react/blob/master/svelte-components/webpack.config.js.

@mvolkmann Webpack can run multiple individual configs at once if you set module.exports to an array of them.

First of all, I had to install the svelte package as well to build correctly, I forgot to mention it in my previous message.

Second, I had to move the font-size style inside the button selector in your Counter.svelte component because it was erroring.

Next, change your package.json's bundle value to just "webpack", and then update your webpack.config.js file to this:

const configs = []
const components = ['Counter', 'Greet']

for (const component of components) {
  configs.push({
    mode: 'development',
    entry: `${__dirname}/src/${component}.svelte`,
    output: {
      path: `${__dirname}/build`,
      filename: `web-component-${component.toLowerCase()}.js`
    },
    module: {
      rules: [
        {
          test: /\.svelte$/,
          exclude: /node_modules/,
          use: {
            loader: 'svelte-loader',
            options: {
              customElement: true
            }
          }
        }
      ]
    }
  });
}

module.exports = configs

Then you can run npm run bundle and see the individual component JS files in your build directory.

That loop is just one approach, but you can of course modify the loop to be as complex as you like.

@mvolkmann It looks like I misread your last message. You want all of the components in a single file.

In that case leave your webpack.config.js as it was but set your entry to the index.js file you have, then change the contents of index.js to:

export { default as Counter } from './Counter.svelte';
export { default as Greet } from './Greet.svelte';

That way you can access them through normal imports, or if it's in the browser you can access your elements via customElements.get('x-counter'), like normal.

Thanks so much for sticking with me through this @jakelucas! It's all working now!

Do you think an example like mine, including the webpack.config.js and the package.json bundle script should appear somewhere in the Svelte documentation? Compiling Svelte components to custom elements and using them in a non-Svelte web app is a really cool feature, but it wasn't easy for me to figure out how to do it. It seems like a small amount of documentation would make it much easier for others.

@mvolkmann I don't know whether it's worth having a full blown tutorial or not, but maybe something to point people in the right direction could be a good thing.

Also, I'm not sure of your reason for using custom elements, but it's worth noting that if you build for the DOM (which is the default) then you can access the components without them being custom elements anyway.

For example if you change your Webpack output to:

  output: {
    path: __dirname + '/build',
    filename: 'web-components.js',
    libraryTarget: 'umd',
    umdNamedDefine: true,
    library: 'svelteComponents'
  }

Then you will be able to access the components like svelteComponents.Greet or svelteComponents.Counter, and use them like normal Svelte components.

It's also worth noting that you're currently only building the DOM/Client-side version of your components. If you want to use them in SSR then you'll also need to build a second server-side version of the same components by setting the generate option in the compiler (via svelte-loader) to 'ssr'.
Once you've done that you need to then load the correct file based on whether your on the server or in the browser.

My reason for wanting to learn how to compile Svelte components to custom elements is only to allow them to be easily used in web apps built with other frameworks like React, Vue, and Angular. I feel like this capability of Svelte is one the things people mention when they are advocating for Svelte and I wanted to make sure I understand how to use that feature.

My preference is to build web apps entirely in Svelte and I wouldn’t use custom elements in that case.

I hadn’t considered building components for SSR. Thanks for sharing that detail. I don’t recall seeing anything about the two options you shared in the official docs (output.library and the generate ssr option). I think those would also make great additions to the docs!

I have struggled with some of the same issues as @mvolkmann.
I think using Svelte for creating autonomous custom elements is a good, but maybe a bit 'premature' idea.
If you try to build a large LOB web application micro front ends is one of the ways to go. I my head Svelte should be a very good fit for this, as you do not have any dependencies to 'frameworks' after build.
Then your application can evolve over time, without worrying about upgrading your framework.

This part is missing from the documentation at: https://svelte.dev/docs#Custom_element_API

To get it working with Rollup instead of (above mentioned) Webpack,

  • you have to add customElement: true, to the rollup.config.js file:
	plugins: [
		svelte({
			customElement: true,
  • then a <svelte:options tag="your-element"> or <svelte:options tag="{null}"> at the top of your .svelte files

  • Then run npm run build

IMHO Fine for simple components, expect some required re-factoring for complex components

HTH

I have been working on a small cli tool to quickly publish svelte components to npm. It compiles into three formats umd, es and svelte. I think it is pretty neat (but I am likely biased). It is called "pelte" - a weird concatenation of publish and svelte.

npm install -g pelte

You compile, bundle and publish with the command below: (No configuration is needed):

pelte ./PathToSvelteComponent.svelte

(The command requires that you are logged in to npm)
Pelte will also create a readme.md for the published package, that describe how to use it.

Pelte will also automatically create a web component, if you add <svelte:options tag="my-svelte-element"/> to the svelte component.

The bundle is normally cleaned up after publish, but you can examine it if you run:
pelte ./PathToSvelteComponent.svelte --keep-bundle --skip-publish

The bundle will be located in a folder with the name of the Svelte component. The folder will also contain a file "index-example-umd.html" that shows the component in action. The html-file shows how to use a UMD module, but if you added the <svelte:options tag="my-svelte-element"/> in your svelte component, you should be able to just add in html-file as well.

You can read more here:
https://www.npmjs.com/package/publish-svelte

Hi, I hope someone can help here... I have done all the steps required.

  1. added the option <svelte:options tag="date-picker" immutable={true}/>
  2. added customElement: true in rollup to plugins: [ svelte()
  3. run npm run build

And I keep having the message:

(!) Plugin svelte: No custom element 'tag' option was specified. To automatically register a custom element, specify a name with a hyphen in it, e.g. <svelte:options tag="my-thing"/>. To hide this warning, use
<svelte:options tag={null}/>

What am I doing wrong?

My index.js file:

export { default as default } from './DatePicker.svelte';

DatePicker.svelte file:

<svelte:options tag="date-picker" immutable={true}/>

<script>
  /* code */

The rollup file:

import svelte from 'rollup-plugin-svelte';
import resolve from '@rollup/plugin-node-resolve';
import pkg from './package.json';

const name = pkg.name
	.replace(/^(@\S+\/)?(svelte-)?(\S+)/, '$3')
	.replace(/^\w/, m => m.toUpperCase())
	.replace(/-\w/g, m => m[1].toUpperCase());

export default {
	input: 'src/index.js',
	output: [
		{ file: pkg.module, 'format': 'es' },
		{ file: pkg.main, 'format': 'umd', name }
	],
	plugins: [
		svelte({
			customElement: true
		}),
		resolve()
	]
};

@thojanssens
did it help to move the customElement flag in the rollup configuration to the compiler options?:

import svelte from 'rollup-plugin-svelte';
import resolve from '@rollup/plugin-node-resolve';
import pkg from './package.json';

const name = pkg.name
	.replace(/^(@\S+\/)?(svelte-)?(\S+)/, '$3')
	.replace(/^\w/, m => m.toUpperCase())
	.replace(/-\w/g, m => m[1].toUpperCase());

export default {
	input: 'src/index.js',
	output: [
		{ file: pkg.module, 'format': 'es' },
		{ file: pkg.main, 'format': 'umd', name }
	],
	plugins: [
		svelte({
                   compilerOptions: {
		    customElement: true
                   }
		}),
		resolve()
	]
};