diff --git a/package-lock.json b/package-lock.json
index ce3b5af..ed13868 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -32163,6 +32163,7 @@
},
"devDependencies": {
"@types/node": "^20.19.43",
+ "normalize-package-data": "^8.0.0",
"playwright": "^1.61.0",
"tsup": "^8.5.1",
"vite": "^6.4.3",
@@ -32756,6 +32757,44 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "packages/vite-plugin/node_modules/hosted-git-info": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz",
+ "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^11.1.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "packages/vite-plugin/node_modules/lru-cache": {
+ "version": "11.5.2",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
+ "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "packages/vite-plugin/node_modules/normalize-package-data": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-8.0.0.tgz",
+ "integrity": "sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "hosted-git-info": "^9.0.0",
+ "semver": "^7.3.5",
+ "validate-npm-package-license": "^3.0.4"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
"packages/vite-plugin/node_modules/vite": {
"version": "6.4.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
diff --git a/packages/vite-plugin/package.json b/packages/vite-plugin/package.json
index 1e648bc..b83a1b1 100644
--- a/packages/vite-plugin/package.json
+++ b/packages/vite-plugin/package.json
@@ -38,6 +38,7 @@
"author": "Netlify Inc.",
"devDependencies": {
"@types/node": "^20.19.43",
+ "normalize-package-data": "^8.0.0",
"playwright": "^1.61.0",
"tsup": "^8.5.1",
"vite": "^6.4.3",
diff --git a/packages/vite-plugin/src/main.test.ts b/packages/vite-plugin/src/main.test.ts
index 4a7091c..782a6b7 100644
--- a/packages/vite-plugin/src/main.test.ts
+++ b/packages/vite-plugin/src/main.test.ts
@@ -9,6 +9,7 @@ import { type Browser, type ConsoleMessage, type Locator, type Page, chromium }
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { type InlineConfig, createServer } from 'vite'
+import { COPY_OVERRIDES_TO_FIXTURES, getPackageOverrides } from '../test/support/getPackageOverrides.js'
import netlify from './main.js'
const PLUGIN_PATH = path.resolve(fileURLToPath(import.meta.url), '../..')
@@ -74,70 +75,76 @@ const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(Number)
const supportsVite7 = (nodeMajor === 20 && nodeMinor >= 12) || nodeMajor >= 22
// Vite 8 uses node:util styleText (Node >= 22)
const supportsVite8 = nodeMajor >= 22
-
-describe.for([['5.0.0'], ['6.0.0'], ...(supportsVite7 ? [['7.0.0']] : []), ...(supportsVite8 ? [['8.0.0']] : [])])(
- 'Vite %s',
- ([viteVersion]) => {
- describe('Plugin constructor', () => {
- test('Is a no-op when running in the Netlify CLI', () => {
- process.env.NETLIFY_DEV = 'true'
-
- expect(netlify()).toEqual([])
- })
+const allOverrides = COPY_OVERRIDES_TO_FIXTURES
+ ? [[getPackageOverrides()]]
+ : [
+ [{ vite: '5.0.0' }],
+ [{ vite: '6.0.0' }],
+ ...(supportsVite7 ? [[{ vite: '7.0.0' }]] : []),
+ ...(supportsVite8 ? [[{ vite: '8.0.0' }]] : []),
+ ]
+
+describe.for(allOverrides)('With overrides %s', ([overrides]) => {
+ describe('Plugin constructor', () => {
+ test('Is a no-op when running in the Netlify CLI', () => {
+ process.env.NETLIFY_DEV = 'true'
+
+ expect(netlify()).toEqual([])
})
+ })
- describe('configureServer', () => {
- test('does not warn on single plugin instance', async () => {
- const mockLogger = createMockViteLogger()
- const { server } = await startTestServer({
- customLogger: mockLogger,
- plugins: [netlify({ middleware: false })],
- })
-
- expect(mockLogger.warn).not.toHaveBeenCalled()
-
- await server.close()
+ describe('configureServer', () => {
+ test('does not warn on single plugin instance', async () => {
+ const mockLogger = createMockViteLogger()
+ const { server } = await startTestServer({
+ customLogger: mockLogger,
+ plugins: [netlify({ middleware: false })],
})
- test('warns on duplicate plugin instances', async () => {
- const mockLogger = createMockViteLogger()
- const { server } = await startTestServer({
- customLogger: mockLogger,
- plugins: [netlify({ middleware: false }), netlify({ middleware: false })],
- })
+ expect(mockLogger.warn).not.toHaveBeenCalled()
- expect(mockLogger.warn).toHaveBeenCalledExactlyOnceWith(
- expect.stringMatching(/Multiple instances of @netlify\/vite-plugin have been loaded/),
- expect.objectContaining({}),
- )
+ await server.close()
+ })
- await server.close()
+ test('warns on duplicate plugin instances', async () => {
+ const mockLogger = createMockViteLogger()
+ const { server } = await startTestServer({
+ customLogger: mockLogger,
+ plugins: [netlify({ middleware: false }), netlify({ middleware: false })],
})
- test('does not warn about duplicate plugin instances when server restarts', async () => {
- const mockLogger = createMockViteLogger()
+ expect(mockLogger.warn).toHaveBeenCalledExactlyOnceWith(
+ expect.stringMatching(/Multiple instances of @netlify\/vite-plugin have been loaded/),
+ expect.objectContaining({}),
+ )
- const firstServer = await startTestServer({
- customLogger: mockLogger,
- plugins: [netlify({ middleware: false })],
- })
- expect(mockLogger.warn).not.toHaveBeenCalled()
- await firstServer.server.close()
+ await server.close()
+ })
- const secondServer = await startTestServer({
- customLogger: mockLogger,
- plugins: [netlify({ middleware: false })],
- })
- expect(mockLogger.warn).not.toHaveBeenCalled()
+ test('does not warn about duplicate plugin instances when server restarts', async () => {
+ const mockLogger = createMockViteLogger()
- await secondServer.server.close()
+ const firstServer = await startTestServer({
+ customLogger: mockLogger,
+ plugins: [netlify({ middleware: false })],
})
+ expect(mockLogger.warn).not.toHaveBeenCalled()
+ await firstServer.server.close()
- test('Populates Netlify runtime environment (globals and env vars)', async () => {
- const fixture = new Fixture()
- .withFile(
- 'vite.config.js',
- `import { defineConfig } from 'vite';
+ const secondServer = await startTestServer({
+ customLogger: mockLogger,
+ plugins: [netlify({ middleware: false })],
+ })
+ expect(mockLogger.warn).not.toHaveBeenCalled()
+
+ await secondServer.server.close()
+ })
+
+ test('Populates Netlify runtime environment (globals and env vars)', async () => {
+ const fixture = new Fixture()
+ .withFile(
+ 'vite.config.js',
+ `import { defineConfig } from 'vite';
import netlify from '@netlify/vite-plugin';
export default defineConfig({
@@ -145,40 +152,40 @@ describe.for([['5.0.0'], ['6.0.0'], ...(supportsVite7 ? [['7.0.0']] : []), ...(s
netlify({ middleware: false })
]
});`,
- )
- .withFile(
- 'index.html',
- `
+ )
+ .withFile(
+ 'index.html',
+ `
Hello World
Hello from the browser
`,
- )
- const directory = await fixture.create()
- await fixture
- .withPackages({
- vite: viteVersion,
- '@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(),
- })
- .create()
-
- const { server } = await startTestServer({
- root: directory,
+ )
+ const directory = await fixture.create()
+ await fixture
+ .withPackages({
+ ...overrides,
+ '@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(),
})
+ .create()
- expect((globalThis as Record).Netlify).toBeInstanceOf(Object)
- expect(process.env).toHaveProperty('NETLIFY_LOCAL', 'true')
- expect(process.env).toHaveProperty('CONTEXT', 'dev')
-
- await server.close()
- await fixture.destroy()
+ const { server } = await startTestServer({
+ root: directory,
})
- test('Prints a basic message on server start', async () => {
- const fixture = new Fixture()
- .withFile(
- 'vite.config.js',
- `import { defineConfig } from 'vite';
+ expect((globalThis as Record).Netlify).toBeInstanceOf(Object)
+ expect(process.env).toHaveProperty('NETLIFY_LOCAL', 'true')
+ expect(process.env).toHaveProperty('CONTEXT', 'dev')
+
+ await server.close()
+ await fixture.destroy()
+ })
+
+ test('Prints a basic message on server start', async () => {
+ const fixture = new Fixture()
+ .withFile(
+ 'vite.config.js',
+ `import { defineConfig } from 'vite';
import netlify from '@netlify/vite-plugin';
export default defineConfig({
@@ -186,51 +193,51 @@ describe.for([['5.0.0'], ['6.0.0'], ...(supportsVite7 ? [['7.0.0']] : []), ...(s
netlify({ middleware: false })
]
});`,
- )
- .withFile(
- 'index.html',
- `
+ )
+ .withFile(
+ 'index.html',
+ `
Hello World
Hello from the browser
`,
- )
- const directory = await fixture.create()
- await fixture
- .withPackages({
- vite: viteVersion,
- '@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(),
- })
- .create()
-
- const mockLogger = createMockViteLogger()
- const { server } = await startTestServer({
- root: directory,
- logLevel: 'info',
- customLogger: mockLogger,
+ )
+ const directory = await fixture.create()
+ await fixture
+ .withPackages({
+ ...overrides,
+ '@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(),
})
+ .create()
+
+ const mockLogger = createMockViteLogger()
+ const { server } = await startTestServer({
+ root: directory,
+ logLevel: 'info',
+ customLogger: mockLogger,
+ })
- expect(mockLogger.error).not.toHaveBeenCalled()
- expect(mockLogger.warn).not.toHaveBeenCalled()
- expect(mockLogger.warnOnce).not.toHaveBeenCalled()
- expect(mockLogger.info).toHaveBeenCalledTimes(2)
- expect(mockLogger.info).toHaveBeenNthCalledWith(1, 'Environment loaded', expect.objectContaining({}))
- expect(mockLogger.info).toHaveBeenNthCalledWith(
- 2,
- '💠Linking this project to a Netlify site lets you deploy your site, use any environment variables \
+ expect(mockLogger.error).not.toHaveBeenCalled()
+ expect(mockLogger.warn).not.toHaveBeenCalled()
+ expect(mockLogger.warnOnce).not.toHaveBeenCalled()
+ expect(mockLogger.info).toHaveBeenCalledTimes(2)
+ expect(mockLogger.info).toHaveBeenNthCalledWith(1, 'Environment loaded', expect.objectContaining({}))
+ expect(mockLogger.info).toHaveBeenNthCalledWith(
+ 2,
+ '💠Linking this project to a Netlify site lets you deploy your site, use any environment variables \
defined on your team and site and much more. Run npx netlify init to get started.',
- expect.objectContaining({}),
- )
+ expect.objectContaining({}),
+ )
- await server.close()
- await fixture.destroy()
- })
+ await server.close()
+ await fixture.destroy()
+ })
- test('Prints a message listing emulated features on server start', async () => {
- const fixture = new Fixture()
- .withFile(
- 'vite.config.js',
- `import { defineConfig } from 'vite';
+ test('Prints a message listing emulated features on server start', async () => {
+ const fixture = new Fixture()
+ .withFile(
+ 'vite.config.js',
+ `import { defineConfig } from 'vite';
import netlify from '@netlify/vite-plugin';
export default defineConfig({
plugins: [
@@ -240,68 +247,68 @@ defined on your team and site and much more. Run npx netlify init to get started
})
]
});`,
- )
- .withFile(
- 'index.html',
- `
+ )
+ .withFile(
+ 'index.html',
+ `
Hello World
Hello from the browser
`,
- )
- const directory = await fixture.create()
- await fixture
- .withPackages({
- vite: viteVersion,
- '@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(),
- })
- .create()
-
- const mockLogger = createMockViteLogger()
- const { server } = await startTestServer({
- root: directory,
- logLevel: 'info',
- customLogger: mockLogger,
- })
-
- expect(mockLogger.error).not.toHaveBeenCalled()
- expect(mockLogger.warn).not.toHaveBeenCalled()
- expect(mockLogger.warnOnce).not.toHaveBeenCalled()
- expect(mockLogger.info).toHaveBeenCalledTimes(3)
- expect(mockLogger.info).toHaveBeenNthCalledWith(1, 'Environment loaded', expect.objectContaining({}))
- expect(mockLogger.info).toHaveBeenNthCalledWith(
- 2,
- 'Middleware loaded. Emulating features: aiGateway, blobs, database, environmentVariables, functions, geolocation, headers, images, redirects, static.',
- expect.objectContaining({}),
- )
- expect(mockLogger.info).toHaveBeenNthCalledWith(
- 3,
- expect.stringContaining('Linking this project to a Netlify site'),
- expect.objectContaining({}),
)
+ const directory = await fixture.create()
+ await fixture
+ .withPackages({
+ ...overrides,
+ '@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(),
+ })
+ .create()
- await server.close()
- await fixture.destroy()
+ const mockLogger = createMockViteLogger()
+ const { server } = await startTestServer({
+ root: directory,
+ logLevel: 'info',
+ customLogger: mockLogger,
})
- describe('Middleware enabled', () => {
- let browser: Browser
- let page: Page
+ expect(mockLogger.error).not.toHaveBeenCalled()
+ expect(mockLogger.warn).not.toHaveBeenCalled()
+ expect(mockLogger.warnOnce).not.toHaveBeenCalled()
+ expect(mockLogger.info).toHaveBeenCalledTimes(3)
+ expect(mockLogger.info).toHaveBeenNthCalledWith(1, 'Environment loaded', expect.objectContaining({}))
+ expect(mockLogger.info).toHaveBeenNthCalledWith(
+ 2,
+ 'Middleware loaded. Emulating features: aiGateway, blobs, database, environmentVariables, functions, geolocation, headers, images, redirects, static.',
+ expect.objectContaining({}),
+ )
+ expect(mockLogger.info).toHaveBeenNthCalledWith(
+ 3,
+ expect.stringContaining('Linking this project to a Netlify site'),
+ expect.objectContaining({}),
+ )
+
+ await server.close()
+ await fixture.destroy()
+ })
- beforeEach(async () => {
- browser = await chromium.launch()
- page = await browser.newPage()
- })
+ describe('Middleware enabled', () => {
+ let browser: Browser
+ let page: Page
- afterEach(async () => {
- await browser.close()
- })
+ beforeEach(async () => {
+ browser = await chromium.launch()
+ page = await browser.newPage()
+ })
+
+ afterEach(async () => {
+ await browser.close()
+ })
- test('Returns static files from project dir', async () => {
- const fixture = new Fixture()
- .withFile(
- 'vite.config.js',
- `import { defineConfig } from 'vite';
+ test('Returns static files from project dir', async () => {
+ const fixture = new Fixture()
+ .withFile(
+ 'vite.config.js',
+ `import { defineConfig } from 'vite';
import netlify from '@netlify/vite-plugin';
export default defineConfig({
@@ -311,61 +318,61 @@ defined on your team and site and much more. Run npx netlify init to get started
})
]
});`,
- )
- .withFile(
- 'index.html',
- `
+ )
+ .withFile(
+ 'index.html',
+ `
Hello World
Hello from the static index.html file
`,
- )
- .withFile(
- 'contact/email.html',
- `
+ )
+ .withFile(
+ 'contact/email.html',
+ `
Contact us via email
Hello from another static file
`,
- )
- .withFile('js/main.js', `console.log('Hello from the browser')`)
- const directory = await fixture.create()
- await fixture
- .withPackages({
- vite: viteVersion,
- '@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(),
- })
- .create()
-
- const { server, url } = await startTestServer({
- root: directory,
+ )
+ .withFile('js/main.js', `console.log('Hello from the browser')`)
+ const directory = await fixture.create()
+ await fixture
+ .withPackages({
+ ...overrides,
+ '@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(),
})
+ .create()
- const response = await page.goto(url)
- expect(response?.status()).toBe(200)
- expect(await response?.text()).toContain('Hello from the static index.html file')
+ const { server, url } = await startTestServer({
+ root: directory,
+ })
- expect(await page.goto(`${url}/js/main.js`).then((r) => r?.text())).toContain('console.log(')
+ const response = await page.goto(url)
+ expect(response?.status()).toBe(200)
+ expect(await response?.text()).toContain('Hello from the static index.html file')
- expect(await page.goto(`${url}/contact/email.html`).then((r) => r?.text())).toContain(
- 'Hello from another static file',
- )
+ expect(await page.goto(`${url}/js/main.js`).then((r) => r?.text())).toContain('console.log(')
- // This is Vite's behavior in dev for "404s"
- const notFoundResponse = await page.goto(`${url}/wp-admin.php`)
- expect(notFoundResponse?.status()).toBe(200)
- expect(await notFoundResponse?.text()).toContain('Hello from the static index.html file')
+ expect(await page.goto(`${url}/contact/email.html`).then((r) => r?.text())).toContain(
+ 'Hello from another static file',
+ )
- await server.close()
- await fixture.destroy()
- })
+ // This is Vite's behavior in dev for "404s"
+ const notFoundResponse = await page.goto(`${url}/wp-admin.php`)
+ expect(notFoundResponse?.status()).toBe(200)
+ expect(await notFoundResponse?.text()).toContain('Hello from the static index.html file')
- test('Returns static files with configured Netlify headers', async () => {
- const fixture = new Fixture()
- .withFile(
- 'netlify.toml',
- `[build]
+ await server.close()
+ await fixture.destroy()
+ })
+
+ test('Returns static files with configured Netlify headers', async () => {
+ const fixture = new Fixture()
+ .withFile(
+ 'netlify.toml',
+ `[build]
publish = "dist"
[[headers]]
for = "/contact/*"
@@ -375,10 +382,10 @@ defined on your team and site and much more. Run npx netlify init to get started
for = "/*"
[headers.values]
"X-NF-Hello" = "world"`,
- )
- .withFile(
- 'vite.config.js',
- `import { defineConfig } from 'vite';
+ )
+ .withFile(
+ 'vite.config.js',
+ `import { defineConfig } from 'vite';
import netlify from '@netlify/vite-plugin';
export default defineConfig({
@@ -388,55 +395,52 @@ defined on your team and site and much more. Run npx netlify init to get started
})
]
});`,
- )
- .withFile(
- 'index.html',
- `
+ )
+ .withFile(
+ 'index.html',
+ `
Hello World
Hello from the static index.html file
`,
- )
- .withFile(
- 'contact/email.html',
- `
+ )
+ .withFile(
+ 'contact/email.html',
+ `
Contact us via email
Hello from another static file
`,
- )
- const directory = await fixture.create()
- await fixture
- .withPackages({
- vite: viteVersion,
- '@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(),
- })
- .create()
-
- const { server, url } = await startTestServer({
- root: directory,
- })
-
- expect(await page.goto(`${url}/contact/email`).then((r) => r?.headers())).toHaveProperty(
- 'x-nf-hello',
- 'world',
- )
- expect(await page.goto(url).then((r) => r?.headers())).toHaveProperty('x-nf-hello', 'world')
- expect(await page.goto(`${url}/contact/email`).then((r) => r?.headers())).toHaveProperty(
- 'x-contact-type',
- 'email',
)
- expect(await page.goto(url).then((r) => r?.headers())).not.toHaveProperty('x-contact-type')
+ const directory = await fixture.create()
+ await fixture
+ .withPackages({
+ ...overrides,
+ '@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(),
+ })
+ .create()
- await server.close()
- await fixture.destroy()
+ const { server, url } = await startTestServer({
+ root: directory,
})
- test('Respects configured Netlify redirects and rewrites', async () => {
- const fixture = new Fixture()
- .withFile(
- 'netlify.toml',
- `[[redirects]]
+ expect(await page.goto(`${url}/contact/email`).then((r) => r?.headers())).toHaveProperty('x-nf-hello', 'world')
+ expect(await page.goto(url).then((r) => r?.headers())).toHaveProperty('x-nf-hello', 'world')
+ expect(await page.goto(`${url}/contact/email`).then((r) => r?.headers())).toHaveProperty(
+ 'x-contact-type',
+ 'email',
+ )
+ expect(await page.goto(url).then((r) => r?.headers())).not.toHaveProperty('x-contact-type')
+
+ await server.close()
+ await fixture.destroy()
+ })
+
+ test('Respects configured Netlify redirects and rewrites', async () => {
+ const fixture = new Fixture()
+ .withFile(
+ 'netlify.toml',
+ `[[redirects]]
status = 301
from = "/contact/e-mail"
to = "/contact/email"
@@ -444,10 +448,10 @@ defined on your team and site and much more. Run npx netlify init to get started
status = 200
from = "/beta/*"
to = "/nextgenv3/:splat"`,
- )
- .withFile(
- 'vite.config.js',
- `import { defineConfig } from 'vite';
+ )
+ .withFile(
+ 'vite.config.js',
+ `import { defineConfig } from 'vite';
import netlify from '@netlify/vite-plugin';
export default defineConfig({
@@ -457,72 +461,70 @@ defined on your team and site and much more. Run npx netlify init to get started
})
]
});`,
- )
- .withFile(
- 'contact/email.html',
- `
+ )
+ .withFile(
+ 'contact/email.html',
+ `
Contact us via email
Hello from the redirect target
`,
- )
- .withFile(
- 'nextgenv3/pricing.html',
- `
+ )
+ .withFile(
+ 'nextgenv3/pricing.html',
+ `
Pricing
Hello from the rewrite target
`,
- )
- const directory = await fixture.create()
- await fixture
- .withPackages({
- vite: viteVersion,
- '@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(),
- })
- .create()
-
- const { server, url } = await startTestServer({
- root: directory,
- })
-
- expect(await page.goto(`${url}/contact/email`).then((r) => r?.text())).toContain(
- 'Hello from the redirect target',
- )
- expect(await page.goto(`${url}/contact/e-mail`).then((r) => r?.text())).toContain(
- 'Hello from the redirect target',
- )
- expect(await page.goto(`${url}/beta/pricing`).then((r) => r?.text())).toContain(
- 'Hello from the rewrite target',
)
+ const directory = await fixture.create()
+ await fixture
+ .withPackages({
+ ...overrides,
+ '@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(),
+ })
+ .create()
- await server.close()
- await fixture.destroy()
+ const { server, url } = await startTestServer({
+ root: directory,
})
- test('Handles Image CDN requests', async () => {
- const IMAGE_WIDTH = 800
- const IMAGE_HEIGHT = 400
+ expect(await page.goto(`${url}/contact/email`).then((r) => r?.text())).toContain(
+ 'Hello from the redirect target',
+ )
+ expect(await page.goto(`${url}/contact/e-mail`).then((r) => r?.text())).toContain(
+ 'Hello from the redirect target',
+ )
+ expect(await page.goto(`${url}/beta/pricing`).then((r) => r?.text())).toContain('Hello from the rewrite target')
- const remoteServer = new HTTPServer(
- createImageServerHandler(() => {
- return { width: IMAGE_WIDTH, height: IMAGE_HEIGHT }
- }),
- )
+ await server.close()
+ await fixture.destroy()
+ })
+
+ test('Handles Image CDN requests', async () => {
+ const IMAGE_WIDTH = 800
+ const IMAGE_HEIGHT = 400
- const remoteServerAddress = await remoteServer.start()
+ const remoteServer = new HTTPServer(
+ createImageServerHandler(() => {
+ return { width: IMAGE_WIDTH, height: IMAGE_HEIGHT }
+ }),
+ )
+
+ const remoteServerAddress = await remoteServer.start()
- const fixture = new Fixture()
- .withFile(
- 'netlify.toml',
- `[images]
+ const fixture = new Fixture()
+ .withFile(
+ 'netlify.toml',
+ `[images]
remote_images = [
"^${remoteServerAddress}/allowed/.*"
]`,
- )
- .withFile(
- 'vite.config.js',
- `import { defineConfig } from 'vite';
+ )
+ .withFile(
+ 'vite.config.js',
+ `import { defineConfig } from 'vite';
import netlify from '@netlify/vite-plugin';
export default defineConfig({
@@ -532,10 +534,10 @@ defined on your team and site and much more. Run npx netlify init to get started
})
]
});`,
- )
- .withFile(
- 'index.html',
- `
+ )
+ .withFile(
+ 'index.html',
+ `
Hello World
@@ -545,58 +547,58 @@ defined on your team and site and much more. Run npx netlify init to get started
`,
- )
- .withFile('local/image.jpg', await generateImage(IMAGE_WIDTH, IMAGE_HEIGHT))
-
- const directory = await fixture.create()
- await fixture
- .withPackages({
- vite: viteVersion,
- '@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(),
- })
- .create()
-
- const mockLogger = createMockViteLogger()
- const { server, url } = await startTestServer({
- root: directory,
- logLevel: 'info',
- customLogger: mockLogger,
+ )
+ .withFile('local/image.jpg', await generateImage(IMAGE_WIDTH, IMAGE_HEIGHT))
+
+ const directory = await fixture.create()
+ await fixture
+ .withPackages({
+ ...overrides,
+ '@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(),
})
+ .create()
- await page.goto(url)
+ const mockLogger = createMockViteLogger()
+ const { server, url } = await startTestServer({
+ root: directory,
+ logLevel: 'info',
+ customLogger: mockLogger,
+ })
- const getImageSize = (locator: Locator) => {
- return locator.evaluate((img: HTMLImageElement) => {
- if (img.naturalWidth === 0 || img.naturalHeight === 0) {
- throw new Error(`Image was not loaded`)
- }
+ await page.goto(url)
- return {
- width: img.naturalWidth,
- height: img.naturalHeight,
- }
- })
- }
+ const getImageSize = (locator: Locator) => {
+ return locator.evaluate((img: HTMLImageElement) => {
+ if (img.naturalWidth === 0 || img.naturalHeight === 0) {
+ throw new Error(`Image was not loaded`)
+ }
+
+ return {
+ width: img.naturalWidth,
+ height: img.naturalHeight,
+ }
+ })
+ }
- expect(await getImageSize(page.locator('#local-image'))).toEqual({ width: 100, height: 50 })
- expect(await getImageSize(page.locator('#allowed-remote-image'))).toEqual({ width: 100, height: 50 })
+ expect(await getImageSize(page.locator('#local-image'))).toEqual({ width: 100, height: 50 })
+ expect(await getImageSize(page.locator('#allowed-remote-image'))).toEqual({ width: 100, height: 50 })
- await expect(
- getImageSize(page.locator('#not-allowed-remote-image')),
- 'Not allowed remote image should not load',
- ).rejects.toThrow(`Image was not loaded`)
+ await expect(
+ getImageSize(page.locator('#not-allowed-remote-image')),
+ 'Not allowed remote image should not load',
+ ).rejects.toThrow(`Image was not loaded`)
- await server.close()
- await fixture.destroy()
- })
+ await server.close()
+ await fixture.destroy()
+ })
- // TODO: Figure out why Blobs is not available in these tests. It works
- // when I test manually on a site.
- test.todo('Handles function requests', async () => {
- const fixture = new Fixture()
- .withFile(
- 'vite.config.js',
- `import { defineConfig } from 'vite';
+ // TODO: Figure out why Blobs is not available in these tests. It works
+ // when I test manually on a site.
+ test.todo('Handles function requests', async () => {
+ const fixture = new Fixture()
+ .withFile(
+ 'vite.config.js',
+ `import { defineConfig } from 'vite';
import netlify from '@netlify/vite-plugin';
export default defineConfig({
@@ -606,20 +608,20 @@ defined on your team and site and much more. Run npx netlify init to get started
})
]
});`,
- )
- .withFile(
- 'index.html',
- `
+ )
+ .withFile(
+ 'index.html',
+ `
Hello World
Hello from the browser
`,
- )
- .withFile(
- 'netlify/functions/blob.mjs',
- `
+ )
+ .withFile(
+ 'netlify/functions/blob.mjs',
+ `
import { getStore } from "@netlify/blobs"
@@ -640,36 +642,36 @@ defined on your team and site and much more. Run npx netlify init to get started
path: "/blob"
}
`,
- )
-
- const directory = await fixture.create()
- await fixture
- .withPackages({
- vite: viteVersion,
- '@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(),
- '@netlify/blobs': pathToFileURL(path.resolve(directory, PLUGIN_PATH, '../blobs')).toString(),
- })
- .create()
-
- const mockLogger = createMockViteLogger()
- const { server, url } = await startTestServer({
- root: directory,
- logLevel: 'info',
- customLogger: mockLogger,
- })
+ )
- expect(await page.goto(`${url}/blob`).then((r) => r?.text())).toContain('Added')
- expect(await page.goto(`${url}/blob`).then((r) => r?.text())).toContain('Blob the builder')
+ const directory = await fixture.create()
+ await fixture
+ .withPackages({
+ ...overrides,
+ '@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(),
+ '@netlify/blobs': pathToFileURL(path.resolve(directory, PLUGIN_PATH, '../blobs')).toString(),
+ })
+ .create()
- await server.close()
- await fixture.destroy()
+ const mockLogger = createMockViteLogger()
+ const { server, url } = await startTestServer({
+ root: directory,
+ logLevel: 'info',
+ customLogger: mockLogger,
})
- test('Handles edge function requests', async () => {
- const fixture = new Fixture()
- .withFile(
- 'vite.config.js',
- `import { defineConfig } from 'vite';
+ expect(await page.goto(`${url}/blob`).then((r) => r?.text())).toContain('Added')
+ expect(await page.goto(`${url}/blob`).then((r) => r?.text())).toContain('Blob the builder')
+
+ await server.close()
+ await fixture.destroy()
+ })
+
+ test('Handles edge function requests', async () => {
+ const fixture = new Fixture()
+ .withFile(
+ 'vite.config.js',
+ `import { defineConfig } from 'vite';
import netlify from '@netlify/vite-plugin';
export default defineConfig({
@@ -679,20 +681,20 @@ defined on your team and site and much more. Run npx netlify init to get started
})
]
});`,
- )
- .withFile(
- 'index.html',
- `
+ )
+ .withFile(
+ 'index.html',
+ `
Hello World
Hello from the browser
`,
- )
- .withFile(
- 'netlify/edge-functions/yell.mjs',
- `
+ )
+ .withFile(
+ 'netlify/edge-functions/yell.mjs',
+ `
export default async (req, context) => {
const res = await context.next()
const text = await res.text()
@@ -704,41 +706,41 @@ defined on your team and site and much more. Run npx netlify init to get started
path: "/*"
}
`,
- )
-
- const directory = await fixture.create()
- await fixture
- .withPackages({
- vite: viteVersion,
- '@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(),
- })
- .create()
-
- const mockLogger = createMockViteLogger()
- const { server, url } = await startTestServer({
- root: directory,
- logLevel: 'info',
- customLogger: mockLogger,
- })
+ )
- expect(await page.goto(url).then((r) => r?.text())).toContain('HELLO FROM THE BROWSER ')
+ const directory = await fixture.create()
+ await fixture
+ .withPackages({
+ ...overrides,
+ '@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(),
+ })
+ .create()
- await server.close()
- await fixture.destroy()
+ const mockLogger = createMockViteLogger()
+ const { server, url } = await startTestServer({
+ root: directory,
+ logLevel: 'info',
+ customLogger: mockLogger,
})
- test('Ignores SPA redirect in dev mode', async () => {
- const fixture = new Fixture()
- .withFile(
- 'netlify.toml',
- `[[redirects]]
+ expect(await page.goto(url).then((r) => r?.text())).toContain('HELLO FROM THE BROWSER ')
+
+ await server.close()
+ await fixture.destroy()
+ })
+
+ test('Ignores SPA redirect in dev mode', async () => {
+ const fixture = new Fixture()
+ .withFile(
+ 'netlify.toml',
+ `[[redirects]]
from = "/*"
to = "/index.html"
status = 200`,
- )
- .withFile(
- 'vite.config.js',
- `import { defineConfig } from 'vite';
+ )
+ .withFile(
+ 'vite.config.js',
+ `import { defineConfig } from 'vite';
import netlify from '@netlify/vite-plugin';
export default defineConfig({
@@ -748,10 +750,10 @@ defined on your team and site and much more. Run npx netlify init to get started
})
]
});`,
- )
- .withFile(
- 'index.html',
- `
+ )
+ .withFile(
+ 'index.html',
+ `
SPA App
@@ -759,39 +761,39 @@ defined on your team and site and much more. Run npx netlify init to get started
`,
- )
- .withFile('src/main.js', `document.getElementById('app').textContent = 'Hello from SPA'`)
- const directory = await fixture.create()
- await fixture
- .withPackages({
- vite: viteVersion,
- '@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(),
- })
- .create()
-
- const { server, url } = await startTestServer({
- root: directory,
+ )
+ .withFile('src/main.js', `document.getElementById('app').textContent = 'Hello from SPA'`)
+ const directory = await fixture.create()
+ await fixture
+ .withPackages({
+ ...overrides,
+ '@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(),
})
+ .create()
- // Any route should render the root index.html (Vite handles it) and JS should execute (which
- // verifies the SPA redirect isn't interfering with loading the .js module).
- await page.goto(`${url}/some-route`)
- await page.waitForSelector('#app')
- expect(await page.textContent('#app')).toBe('Hello from SPA')
-
- await server.close()
- await fixture.destroy()
+ const { server, url } = await startTestServer({
+ root: directory,
})
+
+ // Any route should render the root index.html (Vite handles it) and JS should execute (which
+ // verifies the SPA redirect isn't interfering with loading the .js module).
+ await page.goto(`${url}/some-route`)
+ await page.waitForSelector('#app')
+ expect(await page.textContent('#app')).toBe('Hello from SPA')
+
+ await server.close()
+ await fixture.destroy()
})
+ })
- describe('With @vitejs/plugin-react', () => {
- // TODO(serhalp): Skipping on Windows for now. There's an issue on the GitHub Actions
- // Windows image with resolving the `src/main.jsx` path for some reason.
- test.skipIf(process.platform === 'win32')('Returns static files with configured Netlify headers', async () => {
- const fixture = new Fixture()
- .withFile(
- 'vite.config.js',
- `import { defineConfig } from 'vite';
+ describe('With @vitejs/plugin-react', () => {
+ // TODO(serhalp): Skipping on Windows for now. There's an issue on the GitHub Actions
+ // Windows image with resolving the `src/main.jsx` path for some reason.
+ test.skipIf(process.platform === 'win32')('Returns static files with configured Netlify headers', async () => {
+ const fixture = new Fixture()
+ .withFile(
+ 'vite.config.js',
+ `import { defineConfig } from 'vite';
import netlify from '@netlify/vite-plugin';
import react from '@vitejs/plugin-react';
@@ -803,17 +805,17 @@ defined on your team and site and much more. Run npx netlify init to get started
react(),
]
});`,
- )
- .withFile(
- 'netlify.toml',
- `[[headers]]
+ )
+ .withFile(
+ 'netlify.toml',
+ `[[headers]]
for = "/"
[headers.values]
"X-NF-Hello" = "world"`,
- )
- .withFile(
- 'index.html',
- `
+ )
+ .withFile(
+ 'index.html',
+ `
@@ -824,10 +826,10 @@ defined on your team and site and much more. Run npx netlify init to get started
`,
- )
- .withFile(
- 'src/main.jsx',
- `import { StrictMode } from 'react'
+ )
+ .withFile(
+ 'src/main.jsx',
+ `import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import './index.css'
@@ -837,68 +839,67 @@ defined on your team and site and much more. Run npx netlify init to get started
,
)`,
- )
- .withFile('src/index.css', 'body { color: red }')
- .withFile(
- 'src/App.jsx',
- `import reactLogo from './assets/react.svg'
+ )
+ .withFile('src/index.css', 'body { color: red }')
+ .withFile(
+ 'src/App.jsx',
+ `import reactLogo from './assets/react.svg'
export default () =>
Hello from CSR
`,
- )
- .withFile(
- 'src/assets/react.svg',
- ' ',
- )
- const directory = await fixture.create()
- await fixture
- .withPackages({
- '@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(),
- '@vitejs/plugin-react': '5.2.0',
- react: '19.1.0',
- 'react-dom': '19.1.0',
- vite: viteVersion,
- })
- .create()
-
- const { server, url } = await startTestServer({
- root: directory,
+ )
+ .withFile(
+ 'src/assets/react.svg',
+ ' ',
+ )
+ const directory = await fixture.create()
+ await fixture
+ .withPackages({
+ '@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(),
+ '@vitejs/plugin-react': '5.2.0',
+ react: '19.1.0',
+ 'react-dom': '19.1.0',
+ ...overrides,
})
+ .create()
- const browser = await chromium.launch()
- const page = await browser.newPage()
- const browserErrorLogs: ConsoleMessage[] = []
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- browserErrorLogs.push(msg)
- }
- })
+ const { server, url } = await startTestServer({
+ root: directory,
+ })
- const response = await page.goto(url)
- expect(response?.status()).toBe(200)
- expect(await response?.text()).toContain('Hello from SSR')
- expect(response?.headers()).toHaveProperty('x-nf-hello', 'world')
- expect(await page.innerHTML('html')).toContain('Hello from CSR')
-
- // React SPA mode serves index.html for unknown routes
- const notFoundResponse = await page.goto(`${url}/wp-admin.php`)
- expect(notFoundResponse?.status()).toBe(200)
- expect(await notFoundResponse?.text()).toContain('Hello from SSR')
- expect(await page.innerHTML('html')).toContain('Hello from CSR')
-
- expect(
- browserErrorLogs,
- `Unexpected error logs in browser: ${JSON.stringify(browserErrorLogs, null, 2)}`,
- ).toHaveLength(0)
-
- await server.close()
- await fixture.destroy()
- await browser.close()
+ const browser = await chromium.launch()
+ const page = await browser.newPage()
+ const browserErrorLogs: ConsoleMessage[] = []
+ page.on('console', (msg) => {
+ if (msg.type() === 'error') {
+ browserErrorLogs.push(msg)
+ }
})
+
+ const response = await page.goto(url)
+ expect(response?.status()).toBe(200)
+ expect(await response?.text()).toContain('Hello from SSR')
+ expect(response?.headers()).toHaveProperty('x-nf-hello', 'world')
+ expect(await page.innerHTML('html')).toContain('Hello from CSR')
+
+ // React SPA mode serves index.html for unknown routes
+ const notFoundResponse = await page.goto(`${url}/wp-admin.php`)
+ expect(notFoundResponse?.status()).toBe(200)
+ expect(await notFoundResponse?.text()).toContain('Hello from SSR')
+ expect(await page.innerHTML('html')).toContain('Hello from CSR')
+
+ expect(
+ browserErrorLogs,
+ `Unexpected error logs in browser: ${JSON.stringify(browserErrorLogs, null, 2)}`,
+ ).toHaveLength(0)
+
+ await server.close()
+ await fixture.destroy()
+ await browser.close()
})
})
- },
-)
+ })
+})
diff --git a/packages/vite-plugin/test/support/getPackageOverrides.ts b/packages/vite-plugin/test/support/getPackageOverrides.ts
new file mode 100644
index 0000000..f0b4e1e
--- /dev/null
+++ b/packages/vite-plugin/test/support/getPackageOverrides.ts
@@ -0,0 +1,28 @@
+/**
+ * These utils allow vite-ecosystem-ci to run our tests against arbitrary releases of vite/rolldown/vitest/etc.
+ */
+
+// vite-ecosystem-ci injects overrides in the root package.json
+import rootPkg from '../../../../package.json' with { type: 'json' }
+
+import normalizePackageData, { type Package } from 'normalize-package-data'
+
+export const COPY_OVERRIDES_TO_FIXTURES = process.env.COPY_OVERRIDES_TO_FIXTURES === 'true'
+
+interface PackageWithOverrides extends Package {
+ overrides?: Record
+}
+
+export function getPackageOverrides(): NonNullable {
+ normalizePackageData(rootPkg)
+ const packageJson = rootPkg as unknown as PackageWithOverrides
+
+ if (!COPY_OVERRIDES_TO_FIXTURES) {
+ throw new Error('COPY_OVERRIDES_TO_FIXTURES is not set to true, aborting.')
+ }
+ if (!packageJson.overrides) {
+ throw new Error('No overrides found in package.json, aborting.')
+ }
+
+ return packageJson.overrides
+}
diff --git a/packages/vite-plugin/tsconfig.json b/packages/vite-plugin/tsconfig.json
index 560c467..125535c 100644
--- a/packages/vite-plugin/tsconfig.json
+++ b/packages/vite-plugin/tsconfig.json
@@ -13,5 +13,5 @@
"strict": true,
"skipLibCheck": true
},
- "include": ["src"]
+ "include": ["src", "test"]
}