Skip to content
Open
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 .prettierignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.circleci
.nuxt
dist
coverage
CHANGELOG.md
Expand Down
64 changes: 64 additions & 0 deletions packages/vite-plugin/src/lib/build.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'

import { afterEach, beforeEach, describe, expect, test } from 'vitest'
import { build } from 'vite'

import { createSpaConfigPlugin } from './build.js'

describe('createSpaConfigPlugin', () => {
let root: string

beforeEach(async () => {
root = await realpath(await mkdtemp(join(tmpdir(), 'vite-plugin-netlify-spa-config-')))
await writeFile(join(root, 'index.html'), '<!doctype html><html><body>Hello</body></html>')
})

afterEach(async () => {
await rm(root, { recursive: true, force: true })
})

const configPath = () => join(root, '.netlify/v1/config.json')
const readConfig = async (): Promise<unknown> => JSON.parse(await readFile(configPath(), 'utf8'))

test('writes build.spa = true for a default (SPA) app', async () => {
await build({
root,
logLevel: 'silent',
plugins: [createSpaConfigPlugin()],
})

expect(await readConfig()).toEqual({ build: { spa: true } })
})

test('merges into an existing config.json, preserving other keys', async () => {
await mkdir(join(root, '.netlify/v1'), { recursive: true })
await writeFile(
configPath(),
JSON.stringify({ redirects: [{ from: '/a', to: '/b' }], build: { command: 'npm run build' } }),
)

await build({
root,
logLevel: 'silent',
plugins: [createSpaConfigPlugin()],
})

expect(await readConfig()).toEqual({
redirects: [{ from: '/a', to: '/b' }],
build: { command: 'npm run build', spa: true },
})
})

test('does not write config.json when appType is not spa', async () => {
await build({
root,
logLevel: 'silent',
appType: 'custom',
plugins: [createSpaConfigPlugin()],
})

await expect(readConfig()).rejects.toThrow()
})
})
57 changes: 56 additions & 1 deletion packages/vite-plugin/src/lib/build.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mkdir, writeFile } from 'node:fs/promises'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { join, relative, sep } from 'node:path'
import { sep as posixSep } from 'node:path/posix'

Expand All @@ -10,6 +10,7 @@ import { version, name } from '../../package.json'

// https://docs.netlify.com/frameworks-api/#netlify-v1-functions
const NETLIFY_FUNCTIONS_DIR = '.netlify/v1/functions'
const NETLIFY_CONFIG_PATH = '.netlify/v1/config.json'

const NETLIFY_FUNCTION_FILENAME = 'server.mjs'
const NETLIFY_FUNCTION_DEFAULT_NAME = '@netlify/vite-plugin server handler'
Expand Down Expand Up @@ -97,3 +98,57 @@ export function createBuildPlugin(options?: { displayName?: string; edgeSSR?: bo
},
}
}

const readNetlifyConfig = async (path: string): Promise<Record<string, unknown>> => {
try {
const jsonFile = await readFile(path, 'utf8')
return JSON.parse(jsonFile) as Record<string, unknown>
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return {}
}

throw error
}
}

// Marks the site as a single-page app so the Netlify build system serves `index.html` for all paths
// that don't match a static file, mirroring Vite's own dev server and preview behavior for `appType: 'spa'`.
// https://docs.netlify.com/frameworks-api/#netlify-v1-configjson
export function createSpaConfigPlugin(): Plugin {
let resolvedConfig: ResolvedConfig

return {
apply: 'build',
applyToEnvironment({ name }) {
return name === 'client'
},
configResolved(config) {
resolvedConfig = config
},
name: 'vite-plugin-netlify:spa-config',
async writeBundle() {
if (resolvedConfig.appType !== 'spa') {
return
}

const configPath = join(resolvedConfig.root, NETLIFY_CONFIG_PATH)
const config = await readNetlifyConfig(configPath)

await mkdir(join(resolvedConfig.root, '.netlify/v1'), {
recursive: true,
})

await writeFile(
configPath,
JSON.stringify({
...config,
build: {
...(config.build as Record<string, unknown> | undefined),
spa: true,
},
}),
)
},
}
}
4 changes: 2 additions & 2 deletions packages/vite-plugin/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import dedent from 'dedent'
import type { Plugin } from 'vite'

import { createLoggerFromViteLogger, type Logger } from './lib/logger.js'
import { createBuildPlugin } from './lib/build.js'
import { createBuildPlugin, createSpaConfigPlugin } from './lib/build.js'

export interface NetlifyPluginOptions extends Features {
/**
Expand Down Expand Up @@ -141,7 +141,7 @@ export default function netlify(options: NetlifyPluginOptions = {}): any {
}

const { enabled, ...buildOptions } = options.build ?? {}
return [devPlugin, ...(enabled === true ? [createBuildPlugin(buildOptions)] : [])]
return [devPlugin, createSpaConfigPlugin(), ...(enabled === true ? [createBuildPlugin(buildOptions)] : [])]
}

const warnOnDuplicatePlugin = (logger: Logger) => {
Expand Down
Loading