$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
$ pnpm create svelte@latest
┌ Welcome to SvelteKit!
│
◇ Where should we create your project?
│ (hit Enter to use current directory)
│
◇ Directory not empty. Continue?
│ Yes
│
◇ Which Svelte app template?
│ Skeleton project
│
◇ Add type checking with TypeScript?
│ Yes, using TypeScript syntax
│
◇ Select additional options
│ Add ESLint for code linting, Add Prettier for code formatting
│
└ Your project is ready!
✔ Typescript
Inside Svelte components, use <script lang="ts">
✔ ESLint
https://github.com/sveltejs/eslint-plugin-svelte3
✔ Prettier
https://prettier.io/docs/en/options.html
https://github.com/sveltejs/prettier-plugin-svelte#options
Install community-maintained integrations:
https://github.com/svelte-add/svelte-add
Next steps:
1: npm install (or pnpm install, etc)
2: git init && git add -A && git commit -m "Initial commit" (optional)
3: npm run dev -- --open
To close the dev server, hit Ctrl-C
Stuck? Visit us at https://svelte.dev/chat
{
"scripts": {
"tauri": "cargo tauri dev"
}
}
$ pnpm add -D @sveltejs/adapter-static@next
Then update the adapter import in the svelte.config.js
file:
change
import adapter from '@sveltejs/adapter-auto';
with
// This was changed from adapter-auto
import adapter from '@sveltejs/adapter-static';
Lastly, we need to disable SSR and enable prerendering by adding a root +layout.ts
file (or +layout.js
if you are not using TypeScript) with these contents:
src/routes/+layout.ts
export const prerender = true;
export const ssr = false;
$ cargo install tauri-cli
To scaffold a minimal Rust project that is pre-configured to use Tauri, open a terminal and run the following command:
cargo tauri init
will walk you through a series of questions:
What is your app name? This will be the name of your final bundle and what the OS will call your app. You can use any name you want here.
What should the window title be? This will be the title of the default main window. You can use any title you want here.
Where are your web assets (HTML/CSS/JS) located relative to the <current dir>/src-tauri/tauri.conf.json
file that will be created?
This is the path that Tauri will load your frontend assets from when building for production.
Use ../build
for this value.
What is the URL of your dev server? This can be either a URL or a file path that Tauri will load during development. Use http://localhost:5173 for this value.
What is your frontend dev command?
This is the command used to start your frontend dev server.
Use pnpm dev
(be sure to adapt to use your package manager of choice).
What is your frontend build command?
This is the command to build your frontend files.
Use pnpm build
(be sure to adapt to use your package manager of choice).
$ cargo tauri init
❯ cargo tauri init
✔ What is your app name? · svelte-kit-houdini-typescript-tauri-example
✔ What should the window title be? · sveltekit-houdini-typescript-tauri-example
✔ Where are your web assets (HTML/CSS/JS) located, relative to the "<current dir>/src-tauri/tauri.conf.json" file that will be created? · ../build
✔ What is the url of your dev server? · http://localhost:5173
✔ What is your frontend dev command? · pnpm dev
✔ What is your frontend build command? · pnpm build
$ cargo tauri dev
# or
$ pnpm tauri dev
$ git init
$ git add .
$ git commit -am "first commit"
$ pnpm add -D @skeletonlabs/skeleton
$ pnpm dlx svelte-add@latest tailwindcss
PostCSS
✅ successfully set up!
Create or find an existing issue at https://github.com/svelte-add/svelte-add/issues if this is wrong.
Tailwind CSS
✅ successfully set up!
Create or find an existing issue at https://github.com/svelte-add/svelte-add/issues if this is wrong.
$ pnpm i
devDependencies:
+ autoprefixer 10.4.13
+ postcss 8.4.21
+ postcss-load-config 4.0.1
+ svelte-preprocess 4.10.7 (5.0.1 is available)
+ tailwindcss 3.2.7
Then open your global stylesheet in /src/app.css
and remove the following three @tailwind
directives introduced by Svelte-Add. These will be redudant.
src/app.css
- @tailwind base;
- @tailwind components;
- @tailwind utilities;
leave src/app.css
empty for now, we will use in later to tweak skeleton and other stuff
Apply these following three changes to your tailwind.config.cjs
, found in the root of your project.
const config = {
// apply the dark mode class setting:
darkMode: 'class',
content: [
'./src/**/*.{html,js,svelte,ts}',
// append the path for the Skeleton NPM package and files:
require('path').join(require.resolve('@skeletonlabs/skeleton'),
'../**/*.{html,js,svelte,ts}')
],
theme: {
extend: {}
},
plugins: [
// append the Skeleton plugin to the end of this list
...require('@skeletonlabs/skeleton/tailwind/skeleton.cjs')()
]
};
module.exports = config;
Ensure the following stylesheets are added to your root layout in src/routes/+layout.svelte
Make sure each of these stylesheets are present and in the order shown.
<script>
// skeleton: Make sure each of these stylesheets are present and in the order shown.
// your selected Skeleton theme:
import '@skeletonlabs/skeleton/themes/theme-crimson.css';
// this contains the bulk of Skeletons required styles:
import '@skeletonlabs/skeleton/styles/all.css';
// finally, your application's global stylesheet (sometimes labeled 'app.css')
import '../app.css';
</script>
<slot />
Select a theme, then copy the import statement into your root layout in /src/routes/+layout.svelte
. Replace any existing theme.
we select crimson
, ex @skeletonlabs/skeleton/themes/theme-${THEME}.css
, where ${THEME} is the desired selected theme
import '@skeletonlabs/skeleton/themes/theme-crimson.css';
To enable bonus features (ex: fonts and backgrounds) for preset themes, apply the following attribute in app.html
.
app.html
add data-theme="crimson
into app.html
body
<body data-sveltekit-preload-data="hover" data-theme="crimson">
test skeleton and tailwind
$ pnpm dev
AppShell
to +layout.svelte
+layout.svelte
<script>
// skeleton: Make sure each of these stylesheets are present and in the order shown.
// your selected Skeleton theme:
import '@skeletonlabs/skeleton/themes/theme-crimson.css';
// this contains the bulk of Skeletons required styles:
import '@skeletonlabs/skeleton/styles/all.css';
// finally, your application's global stylesheet (sometimes labeled 'app.css')
import '../app.css';
import { AppShell, AppBar } from '@skeletonlabs/skeleton';
</script>
<!-- App Shell -->
<AppShell>
<svelte:fragment slot="header">
<!-- App Bar -->
<AppBar>
<svelte:fragment slot="lead">
<strong class="text-xl uppercase">Skeleton</strong>
</svelte:fragment>
<svelte:fragment slot="trail">
<a
class="btn btn-sm variant-filled-primary"
href="https://discord.gg/EXqV7W8MtY"
target="_blank"
rel="noreferrer"
>
Discord
</a>
<a
class="btn btn-sm variant-filled-secondary"
href="https://twitter.com/SkeletonUI"
target="_blank"
rel="noreferrer"
>
Twitter
</a>
<a
class="btn btn-sm variant-filled-tertiary"
href="https://github.com/skeletonlabs/skeleton"
target="_blank"
rel="noreferrer"
>
GitHub
</a>
</svelte:fragment>
</AppBar>
</svelte:fragment>
<!-- Page Route Content -->
<slot />
</AppShell>
Let's customize our App Shell's sidebar slot. Open /src/routes/+layout.svelte
and add the following Tailwind utility classes to the AppShell
slotSidebarLeft
prop.
AppShell
change
<AppShell>
to
<AppShell slotSidebarLeft="bg-surface-500/5 w-56 p-4">
Next, let's implement a navigation list within the App Shell's left sidebar slot. Append this slot fragement alongside any other fragment within the AppShell
.
<svelte:fragment slot="sidebarLeft">
<!-- Insert the list: -->
<nav class="list-nav">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/queries">Queries</a></li>
<li><a href="/mutations">Mutations</a></li>
<li><a href="/subscriptions">Subscriptions</a></li>
</ul>
</nav>
<!-- --- -->
</svelte:fragment>
note: /queries, /mutations and /subscriptions routes will be configured in bellow sections
Let's add some basic content to our homepage. Open /src/routes/+page.svelte
and replace the contents with the following. This will provide multiple elements automatically styled by the all.css
stylesheet in our root layout.
all.css
is imported from@skeletonlabs/skeleton/styles/all.css
src/routes/+layout.svelte
<script lang="ts">
import { Avatar } from '@skeletonlabs/skeleton';
import { IconHeart } from '@tabler/icons-svelte';
</script>
<div class="container mx-auto p-8 space-y-8">
<h1>Hello Skeleton</h1>
<p>Lorem ipsum dolor sit amet consectetur, adipisicing elit. Molestiae soluta maxime enim facilis id dolore nobis laborum asperiores qui tenetur deleniti vitae consectetur dignissimos quibusdam, at amet. Earum, vitae delectus!</p>
<p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Voluptatem, cumque consequatur. Debitis at maiores velit repellat! Odit aliquid alias voluptatum delectus? Voluptate inventore laudantium ab commodi alias quos ipsam adipisci.</p>
<section>
<a class="btn variant-filled-primary" href="https://kit.svelte.dev/" target="_blank" rel="noreferrer">SvelteKit</a>
<a class="btn variant-filled-secondary" href="https://tailwindcss.com/" target="_blank" rel="noreferrer">Tailwind</a>
<a class="btn variant-filled-tertiary" href="https://github.com/" target="_blank" rel="noreferrer">GitHub</a>
</section>
<Avatar src="https://i.pravatar.cc/" />
<IconHeart size={48} stroke={1} />
</div>
Finally let's implement Skeleton's Avatar
component. First, import the component, then add it anywhere within your page, we recommend within the .container
element.
src/routes/+page.svelte
<script lang="ts">
...
import { Avatar } from '@skeletonlabs/skeleton';
...
</script>
...
</section>
<Avatar src="https://i.pravatar.cc/" />
</div>
add @tabler/icons-svelte
dependency
$ pnpm add @tabler/icons-svelte
add <IconHeart size={48} stroke={1} />
tabler icon to +page.svelte
src/routes/+page.svelte
<script lang="ts">
...
import { IconHeart } from '@tabler/icons-svelte';
...
</script>
...
<Avatar src="https://i.pravatar.cc/" />
<IconHeart size={48} stroke={1} />
</div>
bellow is the full HomePage
src/routes/+page.svelte
<script lang="ts">
import { Avatar } from '@skeletonlabs/skeleton';
import { IconHeart } from '@tabler/icons-svelte';
</script>
<div class="container mx-auto p-8 space-y-8">
<h1>Hello Skeleton</h1>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
<section>
<a class="btn variant-filled-primary" href="https://kit.svelte.dev/" target="_blank" rel="noreferrer">SvelteKit</a>
<a class="btn variant-filled-secondary" href="https://tailwindcss.com/" target="_blank" rel="noreferrer">Tailwind</a>
<a class="btn variant-filled-tertiary" href="https://github.com/" target="_blank" rel="noreferrer">GitHub</a>
</section>
<Avatar src="https://i.pravatar.cc/" />
<IconHeart size={48} stroke={1} />
</div>
currently skeleton buttons and cards, have big rounded corners, let's tweak it in our app.css
to have small rounded corners, and to test how skeleton styles override works
src/app.css
/* Write your global styles here, in PostCSS syntax */
:root {
--theme-rounded-container: theme(borderRadius.md);
--theme-rounded-base: theme(borderRadius.md);
}
to prevent user select text in tauri apps, in my opinion this is not a good desktop UX
add to bottom of app.css
src/app.css
body {
/* iOS Safari */
-webkit-touch-callout: none;
/* Safari */
-webkit-user-select: none;
/* Konqueror HTML */
-khtml-user-select: none;
/* Firefox */
-moz-user-select: none;
/* Internet Explorer/Edge */
-ms-user-select: none;
/* Non-prefixed version, currently supported by Chrome and Opera */
user-select: none;
}
add LightSwitch
component to toggle from light to dark theme
src/routes/+layout.svelte
add LightSwitch, autoModeWatcher
to existing skeleton imports
then add the following in your root layout template markup.
<svelte:head>{@html `<script>${autoModeWatcher.toString()} autoModeWatcher();</script>`}</svelte:head>
<script>
...
// skeleton imports
import { AppShell, AppBar, LightSwitch, autoModeWatcher } from '@skeletonlabs/skeleton';
...
</script>
<svelte:head>{@html `<script>${autoModeWatcher.toString()} autoModeWatcher();</script>`}</svelte:head>
...
GitHub
</a>
<LightSwitch />
</svelte:fragment>
</AppBar>
...
awesome, now we have one <Avatar />
, one <IconHeart />
and one
Note: currently lightswitch don't work has expected, in my linux system, with OS define in dark, the lightswitch start in dark, but theme shows in light. If we toggle, it works as expected, only the initial start mode is not the correct, it should start in dark and not in light, the problem is in
src/routes/+layout.ts
in lineexport const ssr = false;
, if we comment that line it start work has expected, but tauri recommend that we disable SSR
try to figure it out asap, if anyone know how to fix it, please tell me, really appreciate it
to temporary fix the problem we force the dark mode ate start adding setModeCurrent
to imports and use setModeCurrent(false);
<script>
...
// skeleton imports
import { AppShell, AppBar, LightSwitch, autoModeWatcher, setModeCurrent } from '@skeletonlabs/skeleton';
// force darkMode, until we find how to use autoModeWatcher with SSR disabled
setModeCurrent(false);
...
</script>
create bellow pages to be used with houdini demo
src/routes/queries/+page.svelte
<script lang="ts">
</script>
<div class="container mx-auto p-8 space-y-8">
<section>
<h1>Queries</h1>
<p>page stub</p>
</section>
</div>
src/routes/mutations/+page.svelte
<script lang="ts">
</script>
<div class="container mx-auto p-8 space-y-8">
<section>
<h1>Mutations</h1>
<p>page stub</p>
</section>
</div>
src/routes/subscriptions/+page.svelte
page stub
$ git add .
$ git commit -am "before add graphql server"
run in one window, and leave it open, houdini require it running to do it's magic
$ cd graphql-sse/ && pnpm dev
check running graphql server url at http://localhost:5001/graphql
$ pnpm dlx houdini@latest init
✔ Will you use a remote GraphQL API? … yes
✔ What's the URL for your api? … http://localhost:5001/graphql
🔎 Here's what we found:
✨ SvelteKit
📦 ES Modules
🟦 TypeScript
🚧 Generating project files...
🎩 Welcome to Houdini!
👉 Next Steps
1️⃣ Finalize your installation: pnpm i
2️⃣ Start your application: pnpm dev
# finalize your installation
$ pnpm i
devDependencies:
+ houdini 1.0.7
+ houdini-svelte 1.0.7
# start your application:
$ pnpm dev
VITE v4.1.4 ready in 2400 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h to show help
1:27:25 AM [vite-plugin-svelte] ssr compile done.
package files time avg
sveltekithoudinitypescripttauriexample 1 71.7ms 71.7ms
inspect bellow created and updated files
src/client.ts
$houdini
.graphqlrc.yaml
houdini.config.js
schema.graphql
svelte.config.js
tsconfig.json
vite.config.js
$ tree '$houdini' -L 2
$houdini
├── artifacts
│ └── index.js
├── graphql
│ ├── documents.gql
│ ├── enums.d.ts
│ ├── enums.js
│ ├── index.d.ts
│ ├── index.js
│ └── schema.graphql
├── index.d.ts
├── index.js
├── plugins
│ ├── houdini-svelte
│ ├── index.d.ts
│ └── index.js
├── runtime
│ ├── cache
│ ├── client
│ ├── generated.d.ts
│ ├── generated.js
│ ├── imports
│ ├── index.d.ts
│ ├── index.js
│ ├── lib
│ ├── package.json
│ └── public
└── types
└── src
above pnpm dlx houdini@latest init
command, makes some black magic on our app, creates client, houdini config, types and many other good things
we opted to don't use cache, for this we add defaultCachePolicy: 'NetworkOnly'
in config, to define defaultCachePolicy
globally to whole project
/// <references types="houdini-svelte">
/// docs https://houdinigraphql.com/api/config
/** @type {import('houdini').ConfigFile} */
const config = {
watchSchema: {
url: 'http://localhost:5001/graphql'
},
plugins: {
'houdini-svelte': {}
},
defaultCachePolicy: 'NetworkOnly',
};
export default config;
to see how houdini simplify our lifes, let's populate our recent created pages, but first create BookCard
component needed by queries page
src/components/BookCard.svelte
<script lang='ts'>
import type { Book } from "$lib/types";
export let book: Book;
</script>
<div class="card">
<header class="card-header">{book.title}</header>
<section class="p-4">
<p>{book.description}</p>
<p>author: {book.author}</p>
</section>
<footer class="card-footer">(footer)</footer>
</div>
now replace our queries
src/routes/queries/+page.svelte
<script lang="ts">
import type { PageData } from './$houdini';
import { cache, graphql } from '$houdini';
import { BookCard } from '../../components';
export let data: PageData;
$: ({ Books } = data);
$: console.log(JSON.stringify($Books.data, undefined, 2));
</script>
<div class="container mx-auto p-8 space-y-8">
<section>
<h1 class="mb-5">Queries</h1>
</section>
<main>
<div class="grid sm:gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6">
{#if $Books?.data?.books}
{#each $Books.data.books as book}
{#if book}
<BookCard {book} />
{/if}
{/each}
{/if}
</div>
</main>
</div>
add graphql query
src/routes/queries/+page.gql
query Books {
books {
id
title
author
image
description
price
}
}
with that minimal changes, we have a server side rendering page with a houdini query working, without need to create a +page.server.ts
, it simply works
run app and check results in http://localhost:5173/queries
page, we should see the query result with tow cards
add faker
dependency
$ pnpm add @faker-js/faker
now create mutation +page.svelte
with some minimal CRUD GraphQL operations to test Create, Update and Delete Book's
src/routes/mutations/+page.svelte
<script lang="ts">
import { graphql } from '$houdini';
import { faker } from '@faker-js/faker';
const createBook = graphql(`
mutation CreateBook($input: BookInput!) {
createBook(input: $input) {
id
title
author
image
description
price
}
}
`);
const updateBook = graphql(`
mutation UpdateBook($id: ID!, $input: BookInput!) {
updateBook(id: $id, input: $input) {
id
title
author
image
description
price
}
}
`);
const deleteBook = graphql(`
mutation DeleteBook($id: ID!) {
deleteBook(id: $id) {
id
title
author
image
description
price
}
}
`);
</script>
<div class="container mx-auto p-8 space-y-8">
<section>
<h1>Mutations</h1>
</section>
<main>
<button
class="btn btn-sm variant-filled-tertiary"
on:click={() =>
createBook.mutate({
input: {
title: faker.word.adjective(2),
author: faker.word.adjective(2),
image: faker.image.abstract(640, 360, true),
description: faker.lorem.sentence(5),
price: parseInt(faker.random.numeric(42))
}
})}>Create Book</button
>
<button
class="btn btn-sm variant-filled-tertiary"
on:click={() =>
updateBook.mutate({
id: '3',
input: {
title: faker.word.adjective(2),
author: faker.word.adjective(2),
image: faker.image.abstract(640, 360, true),
description: faker.lorem.sentence(5),
price: parseInt(faker.random.numeric(42))
}
})}>Update Book</button
>
<button
class="btn btn-sm variant-filled-primary"
on:click={() =>
deleteBook.mutate({
id: '3'
})}>Delete Book</button
>
</main>
</div>
change src/client.ts
to
import { HoudiniClient, subscription } from '$houdini';
function sseSockets() {
return {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
subscribe(payload: any, handlers: any) {
const url = new URL('/graphql', 'http://localhost:5001');
url.searchParams.append('query', payload.query);
url.searchParams.append('variables', JSON.stringify(payload.variables));
const eventSource = new EventSource(url);
console.log(`connect to ${url}`);
eventSource.addEventListener('message', (ev) => handlers.next(JSON.parse(ev.data)));
return () => eventSource.close();
},
}
}
export default new HoudiniClient({
url: "http://localhost:5001/graphql",
plugins: [
subscription(sseSockets),
]
})
create subscriptions page +page.svelte
src/routes/subscriptions/+page.svelte
<script lang="ts">
import { graphql } from '$houdini';
// will start listening onMount (browser only)
const updates = graphql(`
subscription NewBooks {
newBooks {
title
author
}
}
`);
$: updates.listen();
$: console.log(`$updates.data: [${JSON.stringify($updates.data)}]`);
</script>
<div class="container mx-auto p-8 space-y-8">
<section>
<h1>Subscriptions</h1>
</section>
<main>
<ul class="li">Last created book</ul>
<ul class="li">title: {$updates?.data?.newBooks.title || 'none'}</ul>
<ul class="li">author: {$updates?.data?.newBooks.author || 'none'}</ul>
</main>
</div>
run app and server if not already running
# run server in terminal window 1
$ cd graphql-sse/ && pnpm dev
# run app in terminal window 2
$ pnpm dev
open two browser windows one with http://localhost:5173/subscriptions page and the other with http://localhost:5173/mutations to test subscriptions, when click Create Book you should see subscrptions page update with the new created book
install sveltekit-superforms
dependency
$ pnpm add sveltekit-superforms zod
add link to NavBar
src/routes/+layout.svelte
<!-- Insert the list: -->
<nav class="list-nav">
<ul>
...
<li><a href="/create-book">Create Book</a></li>
</ul>
</nav>
src/routes/create-book/+page.server.ts
src/routes/create-book/+page.svelte
src/routes/create-book/+page.ts
// prevent Error: Cannot prerender pages with actions
export const prerender = false;
see: