Setup phoenix to use svelte

I Created a blog post about setting up svelte with phoenix.
I found it a bit tricky and the only blog post I found was written using some strange alphabet on medium.
with this setup you can add svelte components to templates with just:

<%= svelte "test", %{:name => "svelte"} %>

At the end I also attached links to gists with diffs against the app - may be helpful. I hope it helps someone.
https://dkuku.github.io/phoenix-svelte-setup

16 Likes

Hi dkuku, thanks a lot for the amazing solution to integrate svelte into phoenix!
I’m new in both frameworks, so i ask you an opinion about the best approach to have, i mean, the best way to use svelte into phoenix is to use it in a widget style letting phoenix manage routing and using it’s templates, or mounting a single page svelte app in layout.html and use phoenix just as an api serving backend?

Sergio

2 Likes

Hi Sergio. This setup is more for small widgets - if you want a true single page app I suggest you to choose a separate backend and frontend repo. Here after the data changes the page gets reloaded. Also you will find more tutorials similiar to your setup when you split the backend and frontend.

1 Like

@dkuku - thank you for the post there. After trying to follow this one:

I followed yours, which I find “better” (use your definition of this word :slight_smile:

I ran however into a problem of:

Uncaught Error: Module parse failed: 'import' and 'export' may only appear at the top level

once I import './svelte.js';

Is this something you’ve seen and solved? Any hints?

Without seeing the actual code, did you put that import statement at the top of the app.js file together with the rest of the imports? This is what the message says anyway.

That’s the whole app.js. I appended the import but I don’t think the “top level” means “top of the file” here, does it?

// We need to import the CSS so that webpack will load it.
// The MiniCssExtractPlugin is used to separate it out into
// its own CSS file.
import "../css/app.scss"

// webpack automatically bundles all modules in your
// entry points. Those entry points can be configured
// in "webpack.config.js".
//
// Import deps with the dep name or local files with a relative path, for example:
//
//     import {Socket} from "phoenix"
//     import socket from "./socket"
//
import "phoenix_html"
import "./svelte.js"

Webpack config, which I supect is the culprit here looks as follows:

const path = require('path');
const glob = require('glob');
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = (env, options) => {
	const devMode = options.mode !== 'production';

	return {
		optimization: {
			minimizer: [
				new TerserPlugin({cache: true, parallel: true, sourceMap: devMode}),
				new OptimizeCSSAssetsPlugin({})
			]
		},
		entry: {
			'app': glob.sync('./vendor/**/*.js').concat(['./js/app.js'])
		},
		output: {
			filename: '[name].js',
			path: path.resolve(__dirname, '../priv/static/js'),
			publicPath: '/js/'
		},
		devtool: devMode ? 'eval-cheap-module-source-map' : undefined,
		resolve: {
			alias: {
				svelte: path.resolve('node_modules', 'svelte')
			},
			extensions: ['.mjs', '.js', '.svelte'],
			mainFields: ['svelte', 'browser', 'module', 'main'],
			modules: ['node_modules']
		},
		module: {
			rules: [
				{
					test: /\.js$/,
					exclude: /node_modules/,
					use: {
						loader: 'babel-loader'
					}
				},
				{
					test: /\.mjs$/,
					include: /node_modules/,
					type: "javascript/auto",
				},
				{
					test: /\.[s]?css$/,
					use: [
						MiniCssExtractPlugin.loader,
						'css-loader',
						'postcss-loader',
						'sass-loader',
					],
				},
				{
					test: /\.(html|svelte)$/,
					use: {
						loader: 'svelte-loader',
						options: {
							emitCss: true,
							hotReload: true
						}
					}
				},
			]
		},
		plugins: [
			new MiniCssExtractPlugin({filename: '../css/app.css'}),
			new CopyWebpackPlugin([{from: 'static/', to: '../'}])
		]
		.concat(devMode ? [new HardSourceWebpackPlugin()] : [])
	}
};

Hard to say what it can be. Looks right, but JS is very fragile. Can you share the actual JS stack trace? Or maybe try this one? GitHub - virkillz/sveltex: Elixir Phoenix + Svelte = ❤️

Here it comes:

Uncaught Error: Module parse failed: 'import' and 'export' may only appear at the top level (40:333)
File was processed with these loaders:
 * ./node_modules/svelte-loader/index.js
You may need an additional loader to handle the result of these loaders.
| 	}
| }
> import * as ___SVELTE_HMR_HOT_API from 'project_root_path/assets/node_modules/svelte-loader/lib/hot-api.js';import ___SVELTE_HMR_HOT_API_PROXY_ADAPTER from '<project_root_path>/assets/node_modules/svelte-hmr/runtime/proxy-adapter-dom.js';if (module && module.hot) { if (false) import.meta.hot.accept(); Hello = ___SVELTE_HMR_HOT_API.applyHmr({ m: module, id: "\"js/svelte/hello.svelte\"", hotOptions: {"preserveLocalState":false,"noPreserveStateKey":["@hmr:reset","@!hmr"],"preserveAllLocalStateKey":"@hmr:keep-all","preserveLocalStateKey":"@hmr:keep","noReload":false,"optimistic":true,"acceptNamedExports":true,"acceptAccessors":true,"injectCss":true,"cssEjectDelay":100,"native":false,"compatVite":false,"importAdapterName":"___SVELTE_HMR_HOT_API_PROXY_ADAPTER","absoluteImports":true,"noOverlay":false}, Component: Hello, ProxyAdapter: ___SVELTE_HMR_HOT_API_PROXY_ADAPTER, acceptable: true, cssId: undefined, nonCssHash: undefined, ignoreCss: true, }); }
| export default Hello;
| 
    at eval (hello.svelte:1)
    at Object../js/svelte/hello.svelte (app.js:164)
    at __webpack_require__ (app.js:20)
    at webpackContext (eval at ./js/svelte sync recursive ^\.\/.*\.svelte$ (app.js:142), <anonymous>:8:9)
    at eval (svelte.js?3a38:51)
    at Array.forEach (<anonymous>)
    at window.onload (svelte.js?3a38:41)
eval @ hello.svelte:1
./js/svelte/hello.svelte @ app.js:164
__webpack_require__ @ app.js:20
webpackContext @ .*\.svelte$?acdf:8
eval @ svelte.js?3a38:51
window.onload @ svelte.js?3a38:41
load (async)
eval @ svelte.js?3a38:40
./js/svelte.js @ app.js:153
__webpack_require__ @ app.js:20
eval @ app.js?7473:1
./js/app.js @ app.js:120
__webpack_require__ @ app.js:20
0 @ app.js:175
__webpack_require__ @ app.js:20
(anonymous) @ app.js:84
(anonymous) @ app.js:87

hello.svelte is the helloworld type of:

<script>
	let name = 'Svelte';
</script>

<h1>Hello from {name}!</h1>

Hmm … Hard to say what it can be. It’s obviously that it’s something in the Webpack config. Are you passing in name as a property to the Svelte component? If yes, then you forgot to prefix let name with export, like export let name =, but I don’t think that should matter. It’s probably something else.

Found the culprit: Error in release 3.1.1 (Module parse failed: 'import' and 'export' may only appear at the top level (138:339)) · Issue #178 · sveltejs/svelte-loader · GitHub

If you happen to update it for Phoenix 1.6 / esbuild, please don’t hesitate to share! :slight_smile: