From 66590567ff4e0b56c3fae452b4dcd01b5932e680 Mon Sep 17 00:00:00 2001 From: Philippe Serhal Date: Thu, 6 Aug 2026 12:07:59 -0400 Subject: [PATCH] test: allow custom fixture dep overrides This is needed to be able to add `@netlify/vite-plugin` to vite-ecosystem-ci: https://github.com/vitejs/vite-ecosystem-ci/pull/444. --- package-lock.json | 39 + packages/vite-plugin/package.json | 1 + packages/vite-plugin/src/main.test.ts | 1023 +++++++++-------- .../test/support/getPackageOverrides.ts | 28 + packages/vite-plugin/tsconfig.json | 2 +- 5 files changed, 581 insertions(+), 512 deletions(-) create mode 100644 packages/vite-plugin/test/support/getPackageOverrides.ts diff --git a/package-lock.json b/package-lock.json index ce3b5af1..ed138684 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 1e648bc4..b83a1b1c 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 4a7091cf..782a6b70 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 00000000..f0b4e1e6 --- /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 560c4671..125535cb 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"] }