diff --git a/package-lock.json b/package-lock.json index 0ca2a8bf..213bff36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21312,14 +21312,6 @@ "node": "^18.14.0 || >=20" } }, - "packages/dev-utils/node_modules/@netlify/types": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || >=20" - } - }, "packages/edge-functions": { "name": "@netlify/edge-functions", "version": "2.17.4", diff --git a/packages/vite-plugin/src/main.test.ts b/packages/vite-plugin/src/main.test.ts index 1fc481a3..3f71e2ad 100644 --- a/packages/vite-plugin/src/main.test.ts +++ b/packages/vite-plugin/src/main.test.ts @@ -776,4 +776,88 @@ defined on your team and site and much more. Run npx netlify init to get started }) }) }) + + describe('configurePreviewServer', { timeout: 15_000 }, () => { + test('Hook exists and is properly configured', () => { + const plugins = netlify({ middleware: false }) + expect(plugins).toHaveLength(1) + expect(plugins[0]).toHaveProperty('configurePreviewServer') + expect(typeof plugins[0].configurePreviewServer).toBe('function') + }) + + test('Calls setupNetlifyEnvironment when invoked', async () => { + // Create a temporary directory for the test + const tmpDir = await import('node:fs/promises').then(async (fs) => { + const os = await import('node:os') + const path = await import('node:path') + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'netlify-test-')) + return tmpDir + }) + + // Create a mock preview server + const mockPreviewServer = { + httpServer: { + once: vi.fn(), + listening: true, + address: () => ({ port: 4173 }), + }, + config: { + logger: { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }, + root: tmpDir, + publicDir: tmpDir + '/public', + }, + middlewares: { + use: vi.fn(), + }, + } + + const plugins = netlify({ middleware: false }) + const plugin = plugins[0] + + try { + // Call the configurePreviewServer hook + await plugin.configurePreviewServer(mockPreviewServer) + + // Verify the httpServer.once was called (part of cleanup setup) + expect(mockPreviewServer.httpServer.once).toHaveBeenCalledWith('close', expect.any(Function)) + } finally { + // Clean up the temporary directory + await import('node:fs/promises').then(async (fs) => { + await fs.rm(tmpDir, { recursive: true }).catch(() => { + // Ignore errors during cleanup + }) + }) + } + }) + + test('Skips setup when httpServer is not available', async () => { + const mockPreviewServer = { + httpServer: null, + config: { + logger: { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }, + root: '/test', + publicDir: '/test/public', + }, + middlewares: { + use: vi.fn(), + }, + } + + const plugins = netlify({ middleware: false }) + const plugin = plugins[0] + + // This should not throw and should return early + await plugin.configurePreviewServer(mockPreviewServer) + + // No assertions needed - just verifying it doesn't crash + }) + }) }) diff --git a/packages/vite-plugin/src/main.ts b/packages/vite-plugin/src/main.ts index f62bba88..eb3d1bb8 100644 --- a/packages/vite-plugin/src/main.ts +++ b/packages/vite-plugin/src/main.ts @@ -14,91 +14,98 @@ export interface NetlifyPluginOptions extends Features { middleware?: boolean } -export default function netlify(options: NetlifyPluginOptions = {}): any { - // If we're already running inside the Netlify CLI, there is no need to run - // the plugin, as the environment will already be configured. - if (process.env.NETLIFY_DEV) { - return [] +async function setupNetlifyEnvironment(server: vite.ViteDevServer | vite.PreviewServer, options: NetlifyPluginOptions) { + // if the server's http server isn't ready (or we're in middleware mode) let's not get involved + if (!server.httpServer) { + return } + + const logger = createLoggerFromViteLogger(server.config.logger) + const { + blobs, + edgeFunctions, + environmentVariables, + functions, + images, + middleware = true, + redirects, + staticFiles, + } = options + + const netlifyDev = new NetlifyDev({ + blobs, + edgeFunctions, + environmentVariables, + functions, + images, + logger, + redirects, + serverAddress: null, + staticFiles: { + ...staticFiles, + directories: [server.config.root, server.config.publicDir], + }, + projectRoot: server.config.root, + }) - const plugin: vite.Plugin = { - name: 'vite-plugin-netlify', - async configureServer(viteDevServer) { - // if the vite dev server's http server isn't ready (or we're in - // middleware mode) let's not get involved - if (!viteDevServer.httpServer) { - return - } - const logger = createLoggerFromViteLogger(viteDevServer.config.logger) - const { - blobs, - edgeFunctions, - environmentVariables, - functions, - images, - middleware = true, - redirects, - staticFiles, - } = options - - const netlifyDev = new NetlifyDev({ - blobs, - edgeFunctions, - environmentVariables, - functions, - images, - logger, - redirects, - serverAddress: null, - staticFiles: { - ...staticFiles, - directories: [viteDevServer.config.root, viteDevServer.config.publicDir], - }, - projectRoot: viteDevServer.config.root, - }) + await netlifyDev.start() - await netlifyDev.start() + server.httpServer.once('close', () => { + netlifyDev.stop() + }) - viteDevServer.httpServer.once('close', () => { - netlifyDev.stop() + logger.log('Environment loaded') + + if (middleware) { + server.middlewares.use(async function netlifyPreMiddleware(nodeReq, nodeRes, next) { + const headers: Record = {} + const result = await netlifyDev.handleAndIntrospectNodeRequest(nodeReq, { + headersCollector: (key, value) => { + headers[key] = value + }, + serverAddress: `http://localhost:${nodeReq.socket.localPort}`, }) - logger.log('Environment loaded') + const isStaticFile = result?.type === 'static' - if (middleware) { - viteDevServer.middlewares.use(async function netlifyPreMiddleware(nodeReq, nodeRes, next) { - const headers: Record = {} - const result = await netlifyDev.handleAndIntrospectNodeRequest(nodeReq, { - headersCollector: (key, value) => { - headers[key] = value - }, - serverAddress: `http://localhost:${nodeReq.socket.localPort}`, - }) + // Don't serve static matches. Let the Vite server handle them. + if (result && !isStaticFile) { + fromWebResponse(result.response, nodeRes) - const isStaticFile = result?.type === 'static' + return + } - // Don't serve static matches. Let the Vite server handle them. - if (result && !isStaticFile) { - fromWebResponse(result.response, nodeRes) + for (const key in headers) { + nodeRes.setHeader(key, headers[key]) + } - return - } + next() + }) - for (const key in headers) { - nodeRes.setHeader(key, headers[key]) - } + logger.log(`Middleware loaded. Emulating features: ${netlifyDev.getEnabledFeatures().join(', ')}.`) + } - next() - }) + if (!netlifyDev.siteIsLinked) { + logger.log( + `💭 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 ${netlifyCommand('npx netlify init')} to get started.`, + ) + } +} - logger.log(`Middleware loaded. Emulating features: ${netlifyDev.getEnabledFeatures().join(', ')}.`) - } +export default function netlify(options: NetlifyPluginOptions = {}): any { + // If we're already running inside the Netlify CLI, there is no need to run + // the plugin, as the environment will already be configured. + if (process.env.NETLIFY_DEV) { + return [] + } - if (!netlifyDev.siteIsLinked) { - logger.log( - `💭 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 ${netlifyCommand('npx netlify init')} to get started.`, - ) - } + const plugin: vite.Plugin = { + name: 'vite-plugin-netlify', + async configureServer(viteDevServer) { + await setupNetlifyEnvironment(viteDevServer, options) + }, + async configurePreviewServer(vitePreviewServer) { + await setupNetlifyEnvironment(vitePreviewServer, options) }, }