I ran into this same issue. When there's no setup needed it's easy enough to do what Rich Harris suggested and export the services from a separate JS file, but I had a wrinkle: lazy loading. Different from your issue, but similar in that the services aren't immediately available.
I wanted to lazy load Firebase with a dynamic import since it's fairly heavy. That meant the services didn't have access to the DB until the import() promise resolved, so exporting them wasn't as straightforward. I found a couple solutions:
If you want to keep the services static, use context
You mentioned a store seems like overkill since services are static. You can use context for this! One catch: setContext needs to be called synchronously during initialization (i.e. not in onMount). If your config happens synchronously you should be able to avoid the extra layer, but here's what I did:
Create an extra component at the top of the app to instantiate the services and pass them as a prop into the root App, where you pass them to setContext. You need the extra component above App to ensure the services exist before setting them in context. Using Svelte's {#await promise} syntax, you can make sure the initialization happens before you pass the prop:
InitServices.svelte
<script>
import config from './config';
import createServices from './services';
import App from './App.svelte';
const services = createServices(config);
</script>
{#await promise then services}
<App {services} />
{/await}
App.svelte
<script>
import Child from './Child.svelte';
export let services;
setContext('services', services);
</script>
<Child /> <-- contains Grandchild.svelte
Grandchild.svelte
<script>
const services = getContext('services');
const promise = services.getData();
</script>
{#await promise then data}
<div>Hello {data.username}!</div>
{/await}
If you have component tests, use a store
Wrinkle #2: I had written some tests with @testing-library/svelte, and they were now complaining about the services coming from getContext since there wasn't a parent calling setContext.
One solution is to create a TestContextWrapper.svelte, which works but adds some extra complexity (not a lot, but it's not nothing). Here's an example of that: https://svelte-recipes.netlify.app/testing/#testing-the-context-api
So... I decided to replace context with a store and it ended up working great. The reactive store is a benefit for testing since you can instantiate the store with mocked services and update it dynamically when you need to.
I still passed the services in as a prop at the top of my app to avoid null checks, but otherwise this cleaned things up a lot.
services-store.js
import { writable } from 'svelte/store';
export const serviceStore = writable(null);
InitServices.svelte
(Same as above)
App.svelte
<script>
import serviceStore from './services-store';
import Child from './Child.svelte';
export let services;
serviceStore.set(services);
</script>
<Child /> <-- contains Grandchild.svelte
Grandchild.svelte
<script>
import serviceStore from './services-store';
const promise = $serviceStore.getData();
</script>
{#await promise then data}
<div>Hello {data.username}!</div>
{:catch err}
<p>Error! {err.message}</p>
{/await}
Grandchild.test.js
import serviceStore from './services-store';
let mockService;
beforeEach(async () => {
mockService = { getData: jest.fn().mockResolvedValue('helloitsjoe') };
serviceStore.set(mockService);
});
...
it('shows username', async () => {
render(Grandchild);
const name = await findByText('helloitsjoe');
expect(name).toBeTruthy();
});
it('shows error', async () => {
// Override mock service
mockService.getData.mockRejectedValue(new Error('oh no!'));
render(Grandchild);
const message = await findByText('oh no!');
expect(message).toBeTruthy();
});