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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ node_modules
dist
*.tgz
coverage
.vite
.vscode
.idea
Thumbs.db
Expand Down
4 changes: 2 additions & 2 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ export function maizzle(configInput?: Partial<MaizzleConfig>): Plugin[] {
maizzleServer = await serve({ config: configInput })

// Clean up when the host server closes
hostServer.httpServer?.on('close', () => {
hostServer.httpServer?.on('close', async () => {
if (maizzleServer) {
maizzleServer.close()
await maizzleServer.close()
maizzleServer = null
}
})
Expand Down
12 changes: 10 additions & 2 deletions src/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export interface ServeOptions {
port?: number
/** Expose the server on the network (e.g. --host) */
host?: boolean | string
/** When true, suppresses the banner/URL output (used by the Vite plugin, which prints its own) */
/** When true, suppresses the startup banner/URL output. */
silent?: boolean
}

Expand Down Expand Up @@ -189,7 +189,11 @@ function maizzleDevPlugin(

const userWatchPaths = config.server?.watch ?? []
const watchPaths = [...defaultWatchPaths, ...userWatchPaths]
const isWatchedFile = createWatchedFileMatcher(watchPaths, config.root ?? process.cwd())
// Match against cwd, not config.root: the watched paths (maizzle/tailwind
// configs, locales) are project-root relative, and `watcher.add` below
// resolves them against cwd too. Using config.root would break matching
// when root points at a subdirectory (e.g. the Vite-plugin setup).
const isWatchedFile = createWatchedFileMatcher(watchPaths, process.cwd())

for (const watchPath of watchPaths) {
server.watcher.add(watchPath)
Expand Down Expand Up @@ -219,6 +223,10 @@ function maizzleDevPlugin(
await renderer.close()
renderer = await createRenderer({ dts: true, markdown: config.markdown, root: config.root, componentDirs: normalizeComponentSources(config.components?.source, process.cwd()), vite: config.vite })

// Re-register the new renderer so user-land render() calls don't keep
// reusing the closed one (see setActiveRenderer above).
setActiveRenderer(renderer)

/**
* Push UI-relevant config bits so the dev UI reacts to live edits
* without a page reload. Uses the same shape as the initial
Expand Down
86 changes: 86 additions & 0 deletions src/tests/serve.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { tmpdir } from 'node:os'
import type { ViteDevServer } from 'vite'
import { serve } from '../serve.ts'
import { getActiveRenderer } from '../render/active.ts'

describe('serve dev server', () => {
let tempDir: string
let server: ViteDevServer | undefined
const originalCwd = process.cwd()

beforeEach(() => {
tempDir = mkdtempSync(join(tmpdir(), 'maizzle-serve-'))
process.chdir(tempDir)
})

afterEach(async () => {
await server?.close()
server = undefined
process.chdir(originalCwd)
rmSync(tempDir, { recursive: true, force: true })
})

it('refreshes the active renderer when the config file changes', async () => {
writeFileSync(join(tempDir, 'maizzle.config.js'), 'export default {}\n')

server = await serve({ port: 3157, silent: true })

const before = getActiveRenderer()
expect(before).toBeTruthy()

// Drive the watcher's config-change branch directly.
server.watcher.emit('change', resolve(tempDir, 'maizzle.config.js'))

await vi.waitFor(() => {
const after = getActiveRenderer()
expect(after).toBeTruthy()
expect(after).not.toBe(before)
}, { timeout: 15000, interval: 100 })
}, 30000)

it('detects a root-level config change when config.root is a subdirectory', async () => {
writeFileSync(join(tempDir, 'maizzle.config.js'), 'export default {}\n')
mkdirSync(join(tempDir, 'emails'), { recursive: true })

server = await serve({ config: { root: 'emails' }, port: 3157, silent: true })

const before = getActiveRenderer()

// Config file lives at cwd, not under root — the watcher must still match it.
server.watcher.emit('change', resolve(tempDir, 'maizzle.config.js'))

await vi.waitFor(() => {
expect(getActiveRenderer()).not.toBe(before)
}, { timeout: 15000, interval: 100 })
}, 30000)

it('clears the active renderer on close', async () => {
server = await serve({ port: 3157, silent: true })
expect(getActiveRenderer()).toBeTruthy()

await server.close()
server = undefined

expect(getActiveRenderer()).toBeNull()
}, 30000)

it('emits a template-updated HMR event when a template changes', async () => {
server = await serve({ port: 3157, silent: true })

const events: string[] = []
const origSend = server.ws.send.bind(server.ws)
server.ws.send = ((payload: any) => {
if (payload?.type === 'custom') events.push(payload.event)
return origSend(payload)
}) as typeof server.ws.send

server.watcher.emit('change', resolve(tempDir, 'emails/welcome.vue'))

await vi.waitFor(() => {
expect(events).toContain('maizzle:template-updated')
}, { timeout: 15000, interval: 100 })
}, 30000)
})
7 changes: 4 additions & 3 deletions src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -717,9 +717,10 @@ export interface MaizzleConfig {
/**
* Vite configuration options passed to the internal Vite SSR server.
*
* Use this to add custom Vite plugins or other Vite options.
* If a `vite.config.{ts,js}` file exists in the project root, it takes
* precedence and this option is used as a fallback.
* Use this to add custom Vite plugins or other Vite options. The internal
* SSR server never loads a project `vite.config.{ts,js}` (it runs with
* `configFile: false`), so pass anything it needs here. These options are
* merged underneath Maizzle's required settings, which take precedence.
*
* @example
* vite: {
Expand Down
13 changes: 12 additions & 1 deletion vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import { defineConfig } from 'vitest/config'
import { defineConfig, coverageConfigDefaults } from 'vitest/config'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
plugins: [vue()],
test: {
globals: true,
environment: 'happy-dom',
coverage: {
exclude: [
...coverageConfigDefaults.exclude,
// Dev-server surfaces: the SSR bootstrap and the dev-UI/middleware
// (rendering, linting, compatibility checks, email sending). These are
// exercised by the running dev server, not unit tests, so they're
// excluded to keep the coverage number meaningful for the library core.
'src/serve.ts',
'src/server/**',
],
},
},
})
Loading