Closed Rich-Harris closed 3 years ago
One idea: since SPA mode basically implies 'static', this becomes an adapter-static
feature:
const adapter = require('@sveltejs/adapter-static');
module.exports = {
kit: {
adapter: adapter({
fallback: '200.html'
},
prerender: {
enabled: false
},
ssr: false
}
};
It would require a new API to be exposed to the prerenderer, but that's technically straightforward.
I'm all for this and was thinking about a way that would allow a true SPA mode to still update dynamically on the basis of CMS provided content.
If I could dream a perfect SPA mode for SvelteKit it would be static files that could optionally still hydrate with up-to-date content (and also dynamic routes) without having to create a different content-fetching technique outside of load
.
edit: A route like
Maybe the blog.svelte
would load the required JS to both render blog.svelte (static) and the JS to hydrate the page(dynamic).load
function could just be left to run on the client instead of restricting it to running only once during the static build?
load
runs on hydration (or navigation) as well as SSR, it's not just using the serialized output of calling it on the server (as was the case with Sapper's preload
). This allows you to have any sort of object as a prop (e.g. you could dynamically import a component inside load
and use it with <svelte:component>
).
The thing that makes the client-side invocation return the same data as the server-side one is that the results of calling fetch
during SSR are serialized and inlined into the page. This ensures consistency when the page hydrates, and saves network round-trips (and also means less data needs to come over the wire, since everything can get compressed using the same gzip dictionary or whatever).
It sounds like you're asking for the ability to turn that inlining behaviour off, which would certainly be possible. We'd just need to add a new option to fetch
— something like this:
export async function load({ fetch }) {
const res = await fetch('/blah.json', { inline: false });
// ...
}
That's something that would warrant a separate issue though.
load runs on hydration (or navigation) as well as SSR, it's not just using the serialized output of calling it on the server (as was the case with Sapper's preload).
thanks for clarifying that for me. I was under the impression that with adapter-static load
would only run during build and not at all on the client.
If load
already runs on the client even in an "exported" adapter-static build, then everything I wanted to express is already possible...
It is, yeah — and I guess it makes sense to note that the { inline: false }
idea above is really just a more formalized way of doing this...
import { browser } from '$app/env';
export async function load({ fetch }) {
const res = await fetch(`/blah.json?inline=${!browser}`);
// ...
}
...since there wouldn't be any inlined data corresponding to /blah.json?inline=false
, which would force SvelteKit to go back to the network.
I think, SPA didn't work after build with adapter-static. This is result after I tried:
index.svelte
:
<h1>index.svelte</h1>
{JSON.stringify(localStorage)}
[slug].svelte
:
<h1>[slug].svelte</h1>
svelte.config.cjs
:
const node = require('@sveltejs/adapter-static');
kit: {
// ...
adapter: node({
fallback: 'index.html',
}),
ssr: false,
// ...
}
vercel.json
:
{
"routes": [
{ "handle": "filesystem" },
{ "src": "/(.*)", "status": 200, "dest": "/" }
]
}
Result for index.svelte
: https://great-book.vercel.app/
Result for [slug].svelte
: https://great-book.vercel.app/whitecoffee-cat-is-cute
The result are same:
index.svelte
{}
that's because fallback
hasn't been implemented yet and you've disabled ssr
?
Is this fallback: https://github.com/mzaini30/great-book/blob/master/src/routes/%24error.svelte
@Rich-Harris
An export which generates static, prerendered files which have static content:
The basic rule is this: for an app to be exportable, any two users hitting the same page of your app must get the same content from the server. In other words, any app that involves user sessions or authentication is not a candidate for sapper export.
Production builds, which generate a Node app which does SSR. The content served through this is dynamic and can be different for users.
Sample directory structure: |
---|
index.html |
js/33jhd.routing.js |
js/93udw.post.route.js |
js/61xae.category.route.js |
css/73jds.layout.css |
onMount
should no longer be needed as all code will be run on the client ONLY.index.svelte
(pseudo-code, not tested), I could expect to write the following code:
<script context="module">
export async function preload({ params }) {
const res = await this.fetch("https://myblog.dev/api/posts/latest")
const data = await res.json();
if (res.status === 200) {
return { latestPost: data };
} else {
this.error(res.status, data.message);
}
}
</script>
<script>
export let latestPost;
</script>
<h1>My latest post on the blog:</h1>
{#if latestPost}
<h2>{latestPost.name}</h2>
<p>{latestPost.tagline}</p>
<a href={"/post/" + latestPost.id}>Read more</a>
{:else}
<p>Loading latest posts...</p>
{/if}
... and then build it to a static page with svelte-kit build --adaptor=semistatic
. This static page would not contain any content, but would update from the server on page load.
The main use case which I can think of for this for is building a client for a backend service which requires authentication, without the complexity of setting up node hosting and server-side rendering (since static hosting is arguably simpler).
These are my thoughts on what a "SPA mode" in SvelteKit should have.
@llui85 I think what you are describing is along the lines of what I wished for in my previous comment.
Dynamic routes should be possible with adapter-static
once the single fallback page from this issue has been implemented
I mostly agree with @llui85. I have the same need. If one could argue that authentication and SSR is a match made in heaven (although it introduces some difficulties and even limitations — not always wanted), I have an e2e encrypted web app on Sapper (and a WIP migration to Kit), so SSR is nothing but meaningless for me — it just serves static files.
I also propose to add pre-built error pages, like 404.html
or 500.html
. I think Kit can get those from prerendering $error.svelte
root component with certain status and message. Not sure if they should be statically pre-rendered or shipped like separate SPA-apps though.
My problem is same 😀 :: dynamic routing ::
@llui85 that was a very long way of asking for what SvelteKit already provides!
@dkzlv
I also propose to add pre-built error pages, like
404.html
or500.html
In an SPA there's no such thing as an error page. Every request (that doesn't match a specific prerendered HTML file) is handled by the same fallback page, usually called index.html
or 200.html
, because there's no way to know if there was an error until the client-side router has run.
Some providers will allow you to specify a fallback 404.html
page if you're prerendering rather than using SPA logic, but that's mutually exclusive with 200.html
. Both pages would have the exact same content since all the work (rendering a content page or an error page) happens in the client. In other words if you were prerendering and wanted a fallback 404 page you'd do it the exact same way:
const adapter = require('@sveltejs/adapter-static');
module.exports = {
kit: {
adapter: adapter({
- fallback: '200.html'
+ fallback: '404.html'
},
prerender: {
enabled: false
},
ssr: false
}
};
Not exactly related to the current discussion, but I'm having an issue when trying to build a static SPA (migrating from a "pure" svelte app, based on the rollup template, and svelte-routing
for routing): some of the imported JS modules are client-only, so assume the existence of window
, document
, etc.
Per the troubleshooting documentation (https://kit.svelte.dev/docs#troubleshooting-server-side-rendering), I should migrate all these top-level import
s to dynamic import('')
s, running either conditional on the browser
flag, or on the onMount
callback?
Unless I'm overlooking something, that will require a rather significant refactoring of all the browser-based library imports, but given I intend to use it for a static SPA, it seems like a hassle to migrate all the imports to dynamic imports just to allow it to run in the server-side prerender.
I guess what I'm looking for is a migration path from Svelte-based SPAs, rather than Sapper projects, when including libraries that rely on a global window
or document
.
The fundamental issue is that the component must be imported in order to determine whether it has an ssr
export. That would continue to be true if we checked each page for ssr
at build time and added the information to a manifest.
There's a couple of options that spring to mind - we find some other way of establishing whether a given page should be SSR'd, or we determine it with static analysis (which has the usual caveats but should be pretty robust in this case).
Having said that, if a module crashes when you merely import it, I would view that as a bug in the module, and raise an issue.
If the whole app has ssr set to false, it doesn't need to get imported and analyzed though, right? Which would prevent the issue. Or is there still some traversal due to the route manifest?
If the whole app has ssr set to false, it doesn't need to get imported and analyzed though, right? Which would prevent the issue. Or is there still some traversal due to the route manifest?
That was exactly my thinking as well. I suggested it in https://github.com/sveltejs/kit/issues/779#issuecomment-810439808 where someone reported the same issue
@Rich-Harris Thanks for the clarifications. I'm not familiar with Surge, Netlify and other providers like that, I'm used to nginx though. Since SPA would still have a bunch of files to serve this thing would work fine, I would only need to adjust the config to serve index.html
from all paths and serve build/assets
folder from /static
path or a subdomain static.
.
@dummdidumm I've tried this and it doesn't really work — or I'm doing something wrong.
I tried this with "@sveltejs/adapter-node": "^1.0.0-next.10"
and "@sveltejs/kit": "^1.0.0-next.64"
.
I set ssr: false
under kit
, but I keep getting build-step errors from this library called nanoid, it's a slim random id generator. It has a runtime check, that crypto
is defined on global
. Node doesn't have it, so I get an infinite loop with this error:
11:17:09 AM [vite] Error when evaluating SSR module /node_modules/nanoid/index.dev.js
Error: Your browser does not have secure random generator. If you don’t need unpredictable IDs, you can use nanoid/non-secure.
at /node_modules/nanoid/index.dev.js:28:11
at instantiateModule (.../vite/dist/node/chunks/dep-0776dd57.js:68919:166) (x163) <--- notice the counter, it goes up like crazy
Should note it never behaved like this in Sapper.
The only thing that worked for me was filtering all these deps that have import problems from noExternal
attribute like this:
/**
* Currently we have problems with 3 deps:
* 1. emoji-regex is not ESM compatible.
* 2. two deps of svelte-i18n: fast-memoize, deepmerge. These are not ESM compatible either.
* 3. nanoid. It keeps throwing errors, because SSR has no secure random generator, lol.
*/
noExternal: Object.keys(pkg.dependencies || {}).filter(
name => !['emoji-regex', 'svelte-i18n', 'nanoid'].includes(name),
),
Should I open a new issue and provide you with a repro?
UPDATE: I saw Rich's response in here, so nevermind.
@dummdidumm @benmccann individual pages could override the app-level setting
Ah, thanks for explaining. I'm wondering if it makes much sense as a page-level setting. I'm assuming that we don't ship it in the manifest in order to be able to decide on each navigation whether the page is server or client rendered. If it takes effect only on the first page load of a site, it seems potentially confusing and I can't think of a use case for it. I imagine that being able to disable it across the board will end up being more requested, but maybe my imagination fails me.
The other option would be to make the setting more than just a boolean. E.g. we could have true
/false
/per-page
or something like that or always
/never
/true-by-default
/false-by-default
or whatever makes sense
These more granular options would at least make it possible to skip traversal to see if invidual pages override the app-level setting, which would prevent the "does not work in node environments" library errors.
This is what I have been looking for for years with Sapper. And it's a game-changing for the Svelte world!
Come on guys! YOU ARE REALLY GORGEOUS!
Hey, just ran into this when i wondered where my index.html is after running npm run build with the static adapter. I didn't quite understand from reading this thread whether it currently is possible to build SPAs with only dynamically routed pages.
Hey, just ran into this when i wondered where my index.html is after running npm run build with the static adapter. I didn't quite understand from reading this thread whether it currently is possible to build SPAs with only dynamically routed pages.
Yes it's possible. I'm using it right now.
The index.html is in _app dir.
The index.html is in _app dir.
Hm, for me it's not. Note that I only have one page which looks like this: [...path].svelte
Naaa. Please install again. Everything works right now with latest versions.
@frederikhors nope, no html in build anywhere. You can try yourself: just create a new sveltekit app, rename src/routes/index.svelte
to src/routes/[...foo].svelte
, also rm -rf build
, install @sveltejs/adapter-static
, configure sveltekit to use it in svelte.config.cjs
and then run npm run build
.
I would imagine this issue would be closed if this would already work.
Sorry I'm still using prerender option... I thought you meant that.
Trying to make libraries work with Vite is a nonstop pain point for people especially with SSR. E.g. see https://github.com/sveltejs/kit/issues/905#issuecomment-816389084 and the corresponding Discord thread. It turns out they had to do a dynamic import
inside an if (browser)
inside onMount
. It took them a long time to figure out and while we could document it, it's rather cumbersome
It seems like it will be impossibly hard to get users that want to run SPA apps to go to all the library authors that wrote client-side only libraries and tell them that they need to add server-side support because their web framework is imposing SSR upon them as they try to build a client-side app.
Anyway, I thought I'd share this user report because I've seen a lot of stuff like this on Discord. The solution is probably to change the way the setting works in one of the ways mentioned in https://github.com/sveltejs/kit/issues/754#issuecomment-811176398, but I'm not sure which is most preferable yet.
Needing to import the templates to resolve the options is also biting a user in https://github.com/sveltejs/kit/issues/933#issuecomment-816820251 where they're trying to build a site with no-prerendered pages but the build process is trying to connect to the production database because we don't know that there are no prerendered pages until we import the site's modules and templates. We would either need to expose these options in another way or add an option to force disablement of prerendering along with adding an option to force disabling of SPA mode. Hopefully there aren't too many of these. It seems like something that could trip up users by default
Yes, TRUE
SPA mode is really what I want. I'm using Svelte in a Restful architecture with non-NodeJS back-end. I wish SvelteKit routing function can display an error page when absent path inputted by user without back-end involvement. Another method I can think is SvelteKit expose an error path
so that back-end redirect to it. Hope this feature can be implemented soon~
I know maintainers hate comments like, "I have this problem too", but I couldn't resist explaining why this is a very important use case for us.
We build self-contained apps using Go and when releasing versions of these apps we embed the files to be served from the binary's embedded file system. Lacking a true SPA mode is what's keeping us from exploring SvelteKit for these sort of deployment scenarios. When we embed the frontend app files, the client has to handle all routing because there is no SSR component.
As an alternative to having an SPA mode we've experimented with SSR using things like v8 bindings in Go but have found that the overhead isn't worth the gain for the purpose of the application, especially since SEO isn't a concern.
@plunkettscott Exactly this! I stumbled upon this issue when i realized i couldn't embed a bundled sveltekit SPA into my crystal binary.
Implemented — docs here: https://github.com/sveltejs/kit/tree/master/packages/adapter-static
Implemented — docs here: https://github.com/sveltejs/kit/tree/master/packages/adapter-static
Thank you! This is was very needed indeed!
@frederikhors nope, no html in build anywhere. You can try yourself: just create a new sveltekit app, rename
src/routes/index.svelte
tosrc/routes/[...foo].svelte
, alsorm -rf build
, install@sveltejs/adapter-static
, configure sveltekit to use it insvelte.config.cjs
and then runnpm run build
.
@repomaa have you found a way to fix this? I'm facing the same issue with my config like below:
import preprocess from 'svelte-preprocess';
import adapter from '@sveltejs/adapter-static';
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: preprocess(),
kit: {
// hydrate the <div id="svelte"> element in src/app.html
target: '#svelte',
adapter: adapter({
// default options are shown
// pages: 'build',
// assets: 'build',
fallback: 'index.html'
}),
prerender: {
enabled: false
},
ssr: false,
vite: {
// root: "./src",
optimizeDeps: {
include: ['clipboard-copy']
},
resolve: {
alias: {
// $components: resolve(__dirname, "./src/components"),
// $stores: resolve(__dirname, "./src/stores"),
$mixlib: resolve(__dirname, "./src/mix.lib.ts"),
},
},
// esbuild: {
// include: /\.(tsx?|jsx?)$/,
// exclude: [],
// loader: 'tsx'
// }
}
}
};
export default config;
@Smilefounder what is the problem. True SPA is working today.
This is something I always wanted, Why have both Svelte and SvelteKit? Why not have just Svelte which can be both at the same time, just Svelte like it is barebones and if you want it to be an App Framework just use the App Framework features, this would make Svelte even more easier to use and userstand because users can go deeper and deeper as they need to. It is the same idea with the library mode which I like you have one framework which can be an app or an component library just apply the same to all around building apps with Svelte.
@ivanjeremic if you use SvelteKit and adapter-static you can have that. And it's wonderful!
@ivanjeremic if you use SvelteKit and adapter-static you can have that. And it's wonderful!
I do need supply a client-side route though? I hope sveltekit can have an option for SPA-mode at npm create
time that will toggle SSR off totally and add a client-side route package, that way, users can choose either SSR or SPA from the start easily.
@laoshaw the client side router is already included in svelte-kit. You can look at my sample in https://github.com/mattiash/svelte-kit-admin-ui It can be built and served by a static http server and access an http api to fetch data.
@mattiash Thanks. It seems you removed a few files from src/route, yes I need do this too, was asking sveltekit to provide an option to have this SPA-mode out-of-box. I'm glad to know client-side-router is built-in, still new to svelte, just came over from vue.
I'm trying to deploy a SvelteKit app for capacitor, since I understood this would work under the hood.
Following the current config
I get errors such as Error: Unexpected option config.kit.ssr
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: [
vitePreprocess(),
preprocess({
postcss: true,
}),
],
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter({
pages: "build",
assets: "build",
fallback: "index.html",
}),
prerender: { entries: [] },
prerender: {
enabled: false,
},
ssr: false,
},
};
export default config;
Has this been removed, if so, that would really suck, since we want one code base for all platforms 🙈
My bad was using the wrong adapter, sorry
// import adapter from "@sveltejs/adapter-node";
import adapter from "@sveltejs/adapter-static";
We now have the
ssr: false
option which gives us something close to SPA mode: https://kit.svelte.dev/docs#ssr-and-javascriptIt doesn't get us all the way there though, because in a typical SPA you would likely want to generate a single fallback page that handles all requests — for example Surge lets you add a 200.html file, while Netlify allows you to add something like this to
_redirects
:By contrast, SvelteKit expects to generate (whether at runtime or prerender time) an HTML page that includes no content, but does include information about the route that should be hydrated, since the router isn't invoked on load. To create a true SPA, SvelteKit needs to create a content-less file that doesn't contain any route information, and the router needs to figure out what JS to load and execute.
I'm not certain how best to do this in a provider-agnostic way.