Skip to content
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

Tru-er SPA mode #1650

Closed
ebeloded opened this issue Jun 8, 2021 · 19 comments · Fixed by #2008 or #2804
Closed

Tru-er SPA mode #1650

ebeloded opened this issue Jun 8, 2021 · 19 comments · Fixed by #2008 or #2804
Labels
feature / enhancement New feature or request p1-important SvelteKit cannot be used by a large number of people, basic functionality is missing, etc.

Comments

@ebeloded
Copy link

ebeloded commented Jun 8, 2021

SvelteKit allows building Single Page Apps with the following config:

const adapter = require('@sveltejs/adapter-static');

module.exports = {
  kit: {
    adapter: adapter({
      fallback: '200.html'
    },
    prerender: {
      enabled: false
    },
    ssr: false
  }
};

Is your feature request related to a problem? Please describe.
Even with the ssr disabled in config file, it is impossible to reference window or document without wrapping it in an environment check. This is because each component needs to be evaluated to determine whether it has SSR export, as @Rich-Harris explained here:

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).

Describe the solution you'd like

I'd like to have a way of configuring SvelteKit as a true SPA, in which I need not worry about referencing browser globals.

As a possible way forward, @benmccann suggested to expand the ssr option, allowing not just a boolean value:

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

I think this would be a fine solution.

Describe alternatives you've considered

The only alternative I currently see is to preface browser globals with environment check and load naughty dependencies dynamically.

How important is this feature to you?
Slightly annoying. Gets in the way. Forces to add meaningless code.

Additional context
The original issue regarding SPA in SvelteKit #754

@umanghome
Copy link

umanghome commented Jun 26, 2021

Since load runs on the browser in SPA mode, it makes sense to be able to use localStorage from within load.

@einarpersson
Copy link

+1

I’d expect it to be possible to import pixi.js when I run SvelteKit in SPA-mode, but right now I have to do a dynamic import await import(“pixi.js”) because Pixi references the window object.

@Rich-Harris
Copy link
Member

Reopening since this isn't actually completely fixed by #2008 — the page's module is still imported, and in a simple test app the server blows up when trying to render a placeholder for a page that imports a client-only dependency

@Rich-Harris Rich-Harris reopened this Jul 31, 2021
@benmccann benmccann added the p1-important SvelteKit cannot be used by a large number of people, basic functionality is missing, etc. label Aug 4, 2021
@lemmon
Copy link

lemmon commented Sep 16, 2021

Since load runs on the browser in SPA mode, it makes sense to be able to use localStorage from within load.

I think there's no need for checking valid variables/libraries. You should be able to reference any browser objects, without warnings, when ssr is set to false.

@brgrz
Copy link

brgrz commented Sep 21, 2021

Agree, this is an issue that still results in compile error with ssr: false in svelte-kit config, for example

Error when evaluating SSR module /src/routes/...
ReferenceError: localStorage is not defined

The app still builds and runs locally but my Render builds are blocked by this.

@JeanJPNM JeanJPNM mentioned this issue Sep 30, 2021
4 tasks
@kilianso
Copy link

kilianso commented Oct 6, 2021

I'm having a hard time because of this using Firebase v9. Even with ssr: false and the browser check, my build fails using the static-adapter. I need to load all of their modules async/dynamically in order to build the app.

@didierfranc
Copy link

didierfranc commented Oct 6, 2021

@kilianso I'm doing that kind of hack waiting a better solution

if (typeof window != 'undefined') {
	this.auth = getAuth(initializeApp(config));
	this.auth.onAuthStateChanged(function (currentUser) {
		if (currentUser) {
			authenticationStatus.set(AUTHENTICATION_STATUS.AUTHENTICATED);
			user.set(currentUser);
		} else {
			authenticationStatus.set(AUTHENTICATION_STATUS.UNAUTHENTICATED);
			user.set(null);
		}
	});
}

@kilianso
Copy link

kilianso commented Oct 6, 2021

@didierfranc I'm doing it by using dynamic imports and separated everything firebase-related into another JS file:

onMount(() => {
	(async () => {
		const {default: firebase} = await import('$lib/utils/firebase');
		firebase();
	})();
})

But even then, and even together with the browser check, if i do something like this: import {getFirestore} from "firebase/firestore"in this dynamically imported JS file, my build still fails with the same erors like the packages would have been imported regularly. I need to load everything dynamically in order to build the app.

@AndrewEQ
Copy link

AndrewEQ commented Nov 9, 2021

I think it would be great to have a more "inline option" like this:

import client { chart } from 'svelte-apexcharts';

Default to existing experience (client & server) unless "client" or "server" is specified.

@dummdidumm
Copy link
Member

dummdidumm commented Nov 9, 2021

That specific syntax is out of scope because it would enhance the JavaScript syntax, which is a no-go. There's a plugin though which does this by adding ?client or ?server to the end of the import string: https://github.com/bluwy/vite-plugin-iso-import . This has other problems though, the biggset one being missing editor support.

@AKuederle
Copy link

If you have imports that are only available on the client, it is very likely that you have entire parts of your javascript that is only expected to run on the client. Would it be feasible to have two separate script tags, one that would only be executed on the client and one that would be executed during SSR?

@AndrewEQ
Copy link

AndrewEQ commented Nov 9, 2021

@dummdidumm yeah I'm currently doing it that way but yes, no editor support and you gotta install plugin and config it... not exactly an intuitive developer experience (DX) and I don't think anyone is expecting something so foundational to be found in the FAQ as well.

@AKuederle I wouldn't mind that either; at least you have a definitive way to separate the execution but it would be great if the DX were seamless...

i.e. without you having to say that the library should be loaded or used in either environment and thereby alleviating the need to define another script tag, so if the library could "automagically" know that it could be loaded via server-side, client-side or both on its own which in a sense is currently happening, just that when something goes wrong, we need the option to disable one or the other. Having said that, perhaps some may want more fine-grain control which is why I suggested it on the import level vs script tag level. Now if the other shimmy with the ?client would have worked with the editor, it would have been fine as well but just not the greatest DX.

@frederikhors

This comment has been minimized.

@IsaacHub

This comment has been minimized.

@frederikhors

This comment has been minimized.

@bluwy

This comment has been minimized.

@frederikhors
Copy link
Contributor

#2937

@surajbarkale
Copy link

After encountering numerous issues, here is the code that worked for me for firebase auth. I had to do the following:

  1. Use Firebase SDK v9
  2. Disable SSR using static adapter
  3. Do not call any firebase functions in variable initialization
  4. Import firebase types separately using import type
firebase.ts
import type { FirebaseApp } from "firebase/app";
import { initializeApp } from "firebase/app";

let app: FirebaseApp = null

export function firebaseApp() {
    if (app == null) {
        app = initializeApp({
            apiKey: "",
            authDomain: "",
            projectId: "",
            storageBucket: "",
            messagingSenderId: "",
            appId: "",
            measurementId: "",
        })
    }
    return app
}
auth.ts
import type { Readable, Subscriber, Unsubscriber } from 'svelte/store'
import { writable } from 'svelte/store'
import { firebaseApp } from './firebase'
import type { User, Auth } from '@firebase/auth'
import { initializeAuth, browserLocalPersistence, onAuthStateChanged } from "@firebase/auth";

let auth: Auth
let authLoaded = false
let isLoaded = writable(authLoaded)

export const isAuthLoaded = { subscribe: isLoaded.subscribe }

export function getAuth(): Auth {
    if (!auth) {
        auth = initializeAuth(firebaseApp(), {
            persistence: browserLocalPersistence,
        })
    }
    return auth
}

export async function getLoadedAuth(): Promise<Auth> {
    const auth = getAuth()
    if (authLoaded) {
        return auth
    }
    return await new Promise((resolve, reject) => {
        const unsubscribe = onAuthStateChanged(
            auth,
            _ => {
                authLoaded = true
                isLoaded.set(true)
                unsubscribe()
                resolve(auth)
            },
            reject
        )
    })
}

let userSubscribers = new Set<Subscriber<User>>()
let unsubscribeUser: Unsubscriber

export const currentUser: Readable<User> = {
    subscribe: (subscriber: Subscriber<User>): Unsubscriber => {
        userSubscribers.add(subscriber)
        const auth = getAuth()
        if (!unsubscribeUser) {
            unsubscribeUser = onAuthStateChanged(auth, user => {
                for (const subscriber of userSubscribers) {
                    subscriber(user)
                }
                if (!authLoaded) {
                    authLoaded = true
                    isLoaded.set(true)
                }
            })
        }
        subscriber(auth.currentUser)
        return () => {
            userSubscribers.delete(subscriber)
            if (userSubscribers.size == 0) {
                unsubscribeUser()
                unsubscribeUser = null
            }
        }
    }
}

@Rich-Harris
Copy link
Member

hey all — flagging this discussion, as people here might have thoughts on it: #7716

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature / enhancement New feature or request p1-important SvelteKit cannot be used by a large number of people, basic functionality is missing, etc.
Projects
None yet