Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 9 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,23 @@ npx sv add @msw/sveltekit
## What you get

- `msw` added as a dev dependency.
- `src/msw/handlers.ts` or `src/msw/handlers.js` with shared request handlers.
- Optional browser setup in `src/msw/browser` and `src/hooks.client`.
- Optional Node setup in `src/msw/node` and `src/hooks.server`.
- Browser worker configuration in `package.json`.
- `src/mocks/handlers.ts` or `src/mocks/handlers.js` with shared request handlers.
- The `msw()` plugin from `msw/vite` added to your Vite config.
- Optional browser and server setup in the corresponding `src/hooks.client` and `src/hooks.server` `init` hooks.
- Types for `virtual:msw` referenced in `src/app.d.ts`.

The hooks load your handlers and enable the `virtual:msw` network during development only. The Vite plugin selects the browser or Node implementation automatically and serves the browser worker, so there is no need to run `msw init` or copy a worker into `static`.

The add-on installs the latest stable MSW release. The Vite integration requires MSW 3.0.0 or later. You can override the dependency with the `MSW_VERSION` environment variable.

## Options

### `environments`

Choose where MSW should run. This is a multiselect option.
Choose where MSW should run during development: `browser`, `node` (the SvelteKit server), or both. This is a multiselect option.

Default: `browser,node`

```shell
npx sv add @msw/sveltekit="environments:browser,node"
```

## Browser worker

If you enable browser mocking, generate the service worker after installing dependencies:

```shell
npx msw init static --save
```
3 changes: 2 additions & 1 deletion jsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"checkJs": true,
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
},
"include": ["src/**/*.js", "tests/**/*.js", "*.js"]
}
166 changes: 83 additions & 83 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineAddon, defineAddonOptions } from 'sv'
import { color, downloadJson, transforms, defineEnv } from './sv-utils.js'
import { color, downloadJson, transforms } from './sv-utils.js'

const options = defineAddonOptions()
const addonOptions = defineAddonOptions()
.add('environments', {
question: 'Where do you want to use MSW?',
type: 'multiselect',
Expand All @@ -16,7 +16,7 @@ const options = defineAddonOptions()
{
value: 'node',
label: 'Node',
hint: 'Intercept the network in Node.js (e.g. server, tests).',
hint: 'Intercept the network in the SvelteKit server.',
},
],
})
Expand All @@ -26,20 +26,10 @@ const FILES = {
handlers: `import { http, HttpResponse } from 'msw/http';

export const handlers = [
http.get('/api/hello', () => {
http.get('*/api/hello', () => {
return HttpResponse.text('Hello world!');
})
];
`,
browser: `import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';

export const worker = setupWorker(...handlers);
`,
node: `import { setupServer } from 'msw/node';
import { handlers } from './handlers';

export const server = setupServer(...handlers);
`,
}

Expand All @@ -48,58 +38,37 @@ export default defineAddon({
alias: 'msw',
shortDescription: 'add Mock Service Worker to your SvelteKit app',
homepage: 'https://mswjs.io',
options,
options: addonOptions,

setup: ({ isKit, unsupported }) => {
if (!isKit) {
unsupported('Requires SvelteKit')
}
},

run: async ({
directory,
file,
language,
options,
sv,
cwd,
dependencyVersion,
}) => {
run: async ({ directory, file, language, options, sv }) => {
const mswVersion = await getMswVersion()
const extension = language === 'ts' ? 'ts' : 'js'
const mocksDirectory = `${directory.src}/msw`

const env = defineEnv({ sv, cwd, dependencyVersion })
const mocksDirectory = `${directory.src}/mocks`

sv.devDependency('msw', mswVersion)

sv.file(file.viteConfig, addVitePlugin())
sv.file(`${mocksDirectory}/handlers.${extension}`, seedFile(FILES.handlers))
sv.file(`${directory.src}/app.d.ts`, addVirtualModuleTypes())

if (options.environments.includes('browser')) {
sv.file(`${mocksDirectory}/browser.${extension}`, seedFile(FILES.browser))
sv.file(file.package, addMswWorkerDirectory())
sv.file(`${directory.src}/hooks.client.${extension}`, addClientHook(env))
sv.file(`${directory.src}/hooks.client.${extension}`, addInitHook())
}

if (options.environments.includes('node')) {
sv.file(`${mocksDirectory}/node.${extension}`, seedFile(FILES.node))
sv.file(`${directory.src}/hooks.server.${extension}`, addServerHook(env))
sv.file(`${directory.src}/hooks.server.${extension}`, addInitHook())
}
},

nextSteps: ({ options }) => {
const steps = [
'Edit your request handlers in ' + color.path('src/msw/handlers'),
]

if (options.environments.includes('browser')) {
steps.push(
`Generate the browser worker with ${color.command('npx msw init static --save')}`,
)
}

return steps
},
nextSteps: () => [
'Edit your request handlers in ' + color.path('src/mocks/handlers'),
],
})

async function getMswVersion() {
Expand All @@ -125,53 +94,84 @@ function seedFile(content) {
})
}

function addMswWorkerDirectory() {
return transforms.json(({ data }) => {
data.msw ??= {}
data.msw.workerDirectory = ['static']
function addVitePlugin() {
return transforms.script(({ ast, js }) => {
const { alias } = js.imports.find(ast, { from: 'msw/vite', name: 'msw' })
if (!alias) {
js.imports.addNamed(ast, { from: 'msw/vite', imports: ['msw'] })
}
js.vite.addPlugin(ast, { code: `${alias ?? 'msw'}()` })
})
}

/**
* @param {ReturnType<typeof defineEnv>} env
*/
function addClientHook(env) {
return transforms.script(({ ast, content, js }) => {
env.importEnv(ast, js, ['dev'])
js.imports.addNamed(ast, {
from: './msw/browser',
imports: ['worker'],
})

if (content.includes('worker.start(')) {
return
function addVirtualModuleTypes() {
return transforms.text(({ content }) => {
if (/\/\/\/\s*<reference\s+types=["']msw\/vite\/client["']/.test(content)) {
return content
}

js.common.appendFromString(ast, {
code: `export async function init() {
if (dev) {
await worker.start();
}
}`,
})
return `/// <reference types="msw/vite/client" />\n${content}`
})
}

/**
* @param {ReturnType<typeof defineEnv>} env
*/
function addServerHook(env) {
return transforms.script(({ ast, js }) => {
env.importEnv(ast, js, ['dev'])
js.imports.addNamed(ast, {
from: './msw/node',
imports: { server: 'msw_server' },
function addInitHook() {
return transforms.script(({ ast, js, comments, content }) => {
let setup = js.common.parseStatement(`if (import.meta.env.DEV) {
const { network } = await import('virtual:msw');
const { handlers } = await import('./mocks/handlers');

network.configure({ handlers });
await network.enable();
}`)
const exported = js.exports.createNamed(ast, {
name: 'init',
// falling back to an empty init that we will populate with the AST changes below
fallback: js.common.parseFromString('const init = async () => {};'),
})

js.common.appendStatement(ast, {
statement: js.common.parseStatement(`if (dev) {
msw_server.listen({ onUnhandledFrame: 'bypass' });
}`),
})
const declaration = exported.declaration
const init =
declaration?.type === 'VariableDeclaration'
? declaration.declarations[0].init
: declaration

if (
!init ||
(init.type !== 'FunctionDeclaration' &&
init.type !== 'FunctionExpression' &&
init.type !== 'ArrowFunctionExpression')
) {
throw new Error('Cannot add MSW to an init hook that is not a function')
}

// converts non async, non block function bodies into block statements
// so we can have multiple statements
init.async = true
if (init.body.type !== 'BlockStatement') {
init.body = {
type: 'BlockStatement',
body: [{ type: 'ReturnStatement', argument: init.body }],
}
if (init.type === 'ArrowFunctionExpression') init.expression = false
}

const existingSetup = init.body.body.find((statement) =>
js.common.areNodesEqual(statement, setup),
)
if (existingSetup) {
setup = existingSetup
} else {
init.body.body.unshift(setup)
}

// let's add a few comments to show why we use import.meta.env.DEV
for (const line of [
'Use import.meta.env.DEV so Vite drops MSW imports before dependency discovery.',
"SvelteKit's dev is folded later and can leave unused MSW assets in production.",
]) {
if (!content.includes(line)) {
comments.add(setup, { type: 'Line', value: ` ${line}` })
}
}
})
}
Loading
Loading