kit: Shim SvelteKit runtime import aliases / Importing `$app/*` fails

Describe the bug As far as I can tell there’s no way to use sveltekit runtime imports (eg: $app/navigation) outside of sveltekit dev/build. Which makes testing virtually impossible. If there is a way to shim these imports outside of the main sveltekit context I haven’t found it, so perhaps documentation is needed.

My particular use-case is with Storybook, where UI components that rely on any sveltekit modules break the whole setup. I tried aliasing them with webpack (pointing to .svelte-kit/dev/...) but that didn’t work either.

Another use-case is publishing components for sveltekit that would need to rely on those imports.

To Reproduce

  1. Setup storybook with Sveltekit
  2. Create a component that imports a runtime module (eg: $app/env)
  3. Run storybook and see if fail (cannot resolve module $app/env)

Severity Not blocking, but makes building a component library with Storybook or other development/testing frameworks impossible. So, severe annoyance?

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 17
  • Comments: 42 (19 by maintainers)

Commits related to this issue

Most upvoted comments

The introduction of $env has made the surface of this issue widen from just $app/*. Without the ability to shim these things unit/component testing becomes increasingly difficult (ref: https://github.com/microsoft/playwright/issues/18465) and, in practice, leads to awkward workarounds in order to properly support testing environments such as Playwright component testing.

Any update on this? I really would love to be able to use Playwright component testing with Sveltekit, but this issue is sadly preventing it and keeping us from being able to adopt Svelte(Kit) on a larger scale…

Now that we’re post v1.0 do we have a better testing story for sveltekit modules? Looks like the .svelte-kit/runtime alias hack is no longer viable, and if we’re going to mock all of these modules (which cover quite a large api surface now) it would be great it sveltekit provided the mocks, either within @sveltejs/kit in an an ‘official’ testing utils package.

It’s kinda crazy that component unit testing doesn’t work OOTB yet with a v1 framework

Here’s the test mock we’ve been using to handle $app/stores. It doesn’t handle things like import.meta.env.

<script>
  import {setContext} from 'svelte';
  import {writable} from 'svelte/store';

  export let Component;

  export let stores = {
    page: writable('/'),
    navigating: writable(null),
    session: writable(null),
  };

  setContext('__svelte__', stores);
</script>

<svelte:component this={Component} {...$$restProps} />

So you just pass it the actual component to be tested and (optionally) the stores you want to use for $app/stores.

EDIT: Added spreading of other props onto the component to be tested.

https://github.com/michaelwooley/storybook-experimental-vite demonstrates setting up Storybook with the new vite.config.js. We still need to figure out how to handle $navigation and $stores

I would also like to use Playwright component testing with SvelteKit. Does anyone know if there is a work-around that works for the case of using Playwright component testing?

Other sveltekit mocking tips:

import.meta.env

  1. Put all import.meta.env usage in one file, I have been naming that file lib/env.js, so I can reference it via $lib/env.js. This way in the majority of situations you can just mock that one file to set the envs you want to use for the test context:
lib/env.js
export const VITE_HASURA_GRAPHQL_URL = import.meta.env.VITE_HASURA_GRAPHQL_URL
export const VITE_HASURA_GRAPHQL_WS_URL = import.meta.env.VITE_HASURA_GRAPHQL_WS_URL
test.js
// ENV mocks
jest.mock('$lib/env', () => ({
  VITE_HASURA_GRAPHQL_URL:
    'http://fakeendpoint.example.com/v1/graphql',
  VITE_HASURA_GRAPHQL_WS_URL:
    'ws://fakeendpoint.example.com/v1/graphql'
}))

$app/navigation.js

jest.mock('$app/navigation.js', () => ({
  goto: jest.fn()
}))

Here’s an example of someone having to mock this stuff out for testing: https://github.com/rossyman/svelte-add-jest/issues/14#issuecomment-891387235

I’d like to voice my support for a feature that would allow libraries to neatly get access to the $app/navigation, $app/stores, and $app/env modules.

As the creator and maintainer of SvelteKitAuth we’re trying to provide a way for users to augment the SvelteKit session with authentication data using the getSession() hook, and upon changes in the session it would be nice to reset it internally instead of expecting users to do something like signOut().then(session.set), and a similar story for routing. Since signIn() either generates a redirect URL or sends a direct fetch() request depending on the payload provided, currently we’re returning the URL and expect users to route themselves with goto() as such:

signIn().then(goto);

Letting libraries handle these things internally means less boilerplate for our users, so getting access to the SvelteKit router in a global module, instead of a scoped one would be useful. This is how other frameworks and libraries such as the Vue Router and React Router handle this, as they make use of the global React instance SvelteKit might have to work around the fact that Svelte doesn’t provide such a thing, but create its own global context or a singleton.

Yeah this isn’t to do with your own custom aliases, but the runtime modules of sveltekit. There needs to be a way to consume or shim them in non-sveltekit contexts like testing. Updated issue title to be clearer on the issue

Hi! As promised, here’s some feedback after using this technique for 2 weeks. It’s working rather well regarding the initial problem of not having a way to mock $app modules. However, we are having some issues:

  • 👎 having test routes is not ideal because it’s not easy to strip them from the bundle, and they cannot be near the components they tests, which make it hard to maintain tests
  • 👎 tests are rather slow and Playwright as a test runner lacks the ability to rerun a test when corresponding code has changed, which is even more of a problem with this technique, so it’s not really fluid to develop like that
  • ⛔ last but not least: we are having lots of flaky tests with Playwright these days, and it’s getting hard to continue. We are considering leaving Playwright and going back to Vitest (+ Testing Library). However, we would still have this $app modules problem with nos solution (as Vitest has no mean to go to a URL to perform tests, nor can it launch a headless browser as Playwright does).

@benmccann I know you’re all very busy and that there’s a lot of work to be done on this framework that we all love. Nevertheless, in the absence of any feedback from the team on this subject, I can’t help but wonder how this work is being prioritized. Indeed, it is (in my opinion) crucial to the widespread adoption of SvelteKit, given that if it’s impossible to properly test our applications (which is unfortunately the case today) then SvelteKit (and therefore Svelte) will probably not be chosen in many contexts.

In any case, that’s the conclusion we’ve reached. Today, we’d like to have some feedback on the implementation of a solution to this problem, as well as a possible delivery target. I’m well aware that the notion of a deadline on such a project is a very complicated one, but we need to know whether we can expect to wait a few weeks, or whether we’re talking months or years, in which case we’ll have to turn to another framework.

I’m not sure I’ll be able to help more directly by contributing code, but if possible please feel free to give some general lines on what needs to be done, in case I or someone passing by can lend a hand on the subject 😉

Thanks again for your work, really, and I hope this topic can move forward! 🙏

As an update to my previous post, I’ve made a lot of new progress, details are here:

https://github.com/sveltejs/kit/issues/19#issuecomment-1041134457

Now supporting:

  • Handling import.meta.env, etc.
  • Handling aliases like $lib and $app/*.
  • Mocking in general (fetch, etc.) Also, mocking of Stores before component rendering to provide context to components tested in isolation.

TBH I am hitting this issue trying to just do basic testing with uvu and typescript. I have ts files that import $app/env and uvu fails to resolve this using ts-node but I know you guys have set testing as a post 1.0. But any hacks or workarounds would be greatly appreciated. https://github.com/sveltejs/kit/issues/19

@benjaminpreiss and anyone using $app in a package released to npm: The application that uses your library has to add the following to their vite.config.js

ssr: {
	noExternal: ['your_package_name'],
}

If you don’t mind using experimental features of the latest Node.js, along with esmodules/import/export, you can use an esmodule loader hook to mock SvelteKit’s $app/navigation-style import aliases.

Rough beginner example link follows. One could mock the module into a no-op, or rewrite it to .svelte-kit/dev/runtime/app for real functionality (after svelte-kit dev has run):

I’ve built a loader for .svelte files, which can also do pre-processing, and no-op imports of .css, pre-processed assets, and now SvelteKit’s import { goto } from '$app/navigation':

This allows me to simply test my .svelte components in node/es6/esm with uvu, like @alexkornitzer but without typescript.

These loader hooks do not chain well yet, so it’s far from a perfect solution, in general.

EDIT: Big update in my next comment below!

@rmunn

This is from a different repo for a client, so it’s private, unfortunately - it’s still on jest@26 / svelte-jester@1, and the “funnel” example was more of an example of event sourcing with svelte than a unit test demonstration - just happened to be public and relevant to some of the recent testing changes.

That said - let me see about pulling out relevant pieces…

Ok - looked … Looks like I just exported/imported load in the server test.

companies/index-server.js

/**
 * @jest-environment jsdom
 */
import '@testing-library/jest-dom/extend-expect'
import { render } from '@testing-library/svelte'
import companiesIndex, { load } from '$routes/companies/index.svelte'
import debug from 'debug'

const log = debug('tests')

log('starting suite routes/companies/index.svelte')

// Sveltekit Mocks
jest.mock('$app/env.js', () => ({
  amp: false,
  browser: false,
  dev: true,
  mode: 'test'
}))

jest.mock('$app/navigation.js', () => ({
  goto: jest.fn()
}))

// In more recent tests I've started using the "TestHarness" instead of this `svelte` mock with a fake getContext
jest.mock('svelte', () => {
  const { writable } = require('svelte/store')
  const actualSvelte = jest.requireActual('svelte')
  const fakeGetContext = jest.fn((name) => {
    if (name === '__svelte__') {
      return fakeSvelteKitContext
    }
  })
  const fakeSvelteKitContext = {
    page: writable({
      path: '/',
      query: new URLSearchParams({
        offset: 0,
        limit: 5
      })
    }),
    navigating: writable(false)
  }

  const mockedSvelteKit = {
    ...actualSvelte,
    getContext: fakeGetContext
  }
  return mockedSvelteKit
})
// End Sveltekit mocks

// ENV mocks
jest.mock('$lib/env', () => ({
  VITE_PRESIDIO_HASURA_GRAPHQL_URL:
    'http://fakeendpoint.example.com/v1/graphql',
  VITE_PRESIDIO_HASURA_GRAPHQL_INTERNAL_URL:
    'http://fakeendpoint.example.com/v1/graphql',
  VITE_PRESIDIO_HASURA_GRAPHQL_WS_URL:
    'ws://fakeendpoint.example.com/v1/graphql'
}))

// Network mocks
jest.mock('$lib/data/urql', () => ({
  client: {
    query: jest.fn(() => {
      const result = {
        data: {
          companies: [
            {
              id: 'test-1',
              name: 'Test 1',
              logo_url:
                'https://res.cloudinary.com/crunchbase-production/image/upload/v1418896144/nzn3gfio6p8lupehf6nv.jpg',
              __typename: 'companies'
            },
            {
              id: 'test-2',
              name: 'Test 2',
              logo_url:
                'https://res.cloudinary.com/crunchbase-production/image/upload/v1397199104/34852c1debc24e028c4082caa0efb427.jpg',
              __typename: 'companies'
            },
            {
              id: 'test-3',
              name: 'Test 3',
              logo_url:
                'https://res.cloudinary.com/crunchbase-production/image/upload/rswshbdwsa7bg39kjtjk',
              __typename: 'companies'
            }
          ],
          companies_aggregate: {
            aggregate: {
              count: 3
            }
          }
        }
      }

      return {
        toPromise: jest.fn(() => Promise.resolve(result))
      }
    })
  }
}))

// mock store that uses URL query string
jest.mock('$lib/stores/queryStore')

const ctx = {
  page: {
    query: {
      get: jest.fn((key) => {
        const params = {
          limit: 50,
          isClientFilter: true,
          order: 'asc',
          offset: 0
        }

        return params[key]
      })
    }
  }
}

describe('routes/companiesIndex.svelte - server', () => {
  // browser false is default mock

  describe('server side rendering', () => {
    it('should server render empty', async () => {
      const { getByText } = render(companiesIndex)
      expect(getByText('Companies')).toBeInTheDocument()
      expect(getByText('No companies')).toBeInTheDocument()
    })

    it('should server render empty with data but empty companies result', async () => {
      const props = await load(ctx)
      props.companies = []
      const { getByText } = render(companiesIndex, { props })
      expect(getByText('Companies')).toBeInTheDocument()
      expect(getByText('No companies')).toBeInTheDocument()
    })

    it('should server render with data', async () => {
      const { getByText } = render(companiesIndex, await load(ctx))
      expect(getByText('Companies')).toBeInTheDocument()
      expect(getByText('Test 1')).toBeInTheDocument()
    })
  })

  describe('#load', () => {
    it('should query graphql endpoint and return found companies', async () => {
      const { client } = require('$lib/data/urql')
      let result = await load(ctx)
      expect(client.query).toBeCalled()
      expect(result.props.companies.length).toBe(3)
    })
  })
})

And the load function from that component:

  export async function load({ page }) {
    const variables = {
      limit: parseInt(page.query.get('limit'), 10) || defaults.limit,
      isClientFilter: page.query.get('isClientFilter') !== 'false',
      nameFilter: `%${page.query.get('nameFilter') || ''}%`,
      order: page.query.get('order') || 'asc',
      offset: parseInt(page.query.get('offset'), 10) || defaults.offset
    }

    const result = await client.query(QUERY, variables).toPromise()
    const { data } = result
    const { companies } = data

    return {
      props: {
        companies,
        count: data.companies_aggregate.aggregate.count
      }
    }
  }

.babelrc

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "node": "current"
        }
      }
    ]
  ]
}

jest.json

{
  "roots": ["<rootDir>/src", "<rootDir>/__tests__/unit"],
  "testEnvironment": "node",
  "modulePaths": ["<rootDir>/src"],
  "moduleDirectories": ["node_modules"],
  "transform": {
    "^.+\\.svelte$": "svelte-jester",
    "^.+\\.(ts|tsx|js|jsx)$": ["esbuild-jest"]
  },
  "moduleFileExtensions": ["js", "svelte"],
  "moduleNameMapper": {
    "^\\$app(.*)$": "<rootDir>/.svelte-kit/build/runtime/app$1",
    "^\\$lib(.*)$": "<rootDir>/src/lib$1",
    "^\\$routes(.*)$": "<rootDir>/src/routes$1"
  },
  "setupFilesAfterEnv": ["@testing-library/jest-dom/extend-expect"],
  "coverageThreshold": {
    "global": {
      "branches": 0,
      "functions": 0,
      "lines": 0,
      "statements": 0
    }
  },
  "collectCoverageFrom": ["src/**/*.{js,svelte}"],
  "testTimeout": 30000
}

I’ve fixed this in storybook by using their new vite-builder and adding manual aliases to sveltekit’s $app runtime module

async viteFinal(config) {
    config.resolve.alias = {
        $app: path.resolve('./.svelte-kit/dev/runtime/app')
    }

    return config;
}

With the caveat being that you have to have run sveltekit dev first to generate those runtime modules. I think this is worth documenting (the path to the alias if nothing else) for others that need to shim these modules until Svelte comes up with an official workaround

Ah, I see the challenge - the modules like $app/env are the problem, not just aliases. My workaround won’t help there.

Hey all! I have been searching for the correct issue, but all other issues about importing $app/stores in a library build are sadly closed and have not lead to a solution.

Currently, I am getting the error Cannot find package '$app' imported from /Users/benjaminpreiss/Documents/work/montee/node_modules/@frontline-hq/sveltekit-i18n/index.js for this line of code in my sveltekit library: https://github.com/frontline-hq/sveltekit-i18n/blob/09b9c7b62e17661a7d5c8dbf1585132ff7b29f0a/src/lib/index.ts#L2

Does anybody know how to enable $app/stores imports in library build specifically for sveltekit?

Most aliases are now supported out-of-the-box with the latest Storybook 7. You can see a summary of what is supported and not supported here: https://github.com/storybookjs/storybook/tree/next/code/frameworks/sveltekit

There are a few that are not yet supported and make more sense to support as mocks. I’ve created a new issue to track that: https://github.com/storybookjs/storybook/issues/20999. PRs for it would be very welcome!

There’s a great example of Vitest mocks here: https://github.com/sveltejs/kit/issues/5525#issuecomment-1186390654

I think there’s a couple ways to do test setup thus far:

I’m not sure what the tradeoffs are and which is the better approach.

Also, if there’s anything SvelteKit can do to make testing easier I’d be happy to support changes there.

Related, there’s a request to mock fetch (https://github.com/sveltejs/kit/issues/19#issuecomment-914178415), which I haven’t seen anyone do yet.