Skip to content

Commit de9b46c

Browse files
authored
fix: revamp dev and vite plugin logging (#269)
* fix: revamp dev and vite plugin logging - hook into the vite logger when using the vite plugin - fix weird unicode issue - use terminal colour lib with a smaller footprint - remove an extraneous logger util - add a basic message to inform the user that the Netlify Vite plugin loaded - when using middleware mode, add a message informing the user that this is loaded and listing enables features - add test coverage for logs * fix: highlight commands * fix: tweak log copy * style: adjust fixture file indent
1 parent ae9bcfe commit de9b46c

File tree

11 files changed

+196
-23
lines changed

11 files changed

+196
-23
lines changed

package-lock.json

Lines changed: 14 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/dev-utils/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
},
5353
"dependencies": {
5454
"@whatwg-node/server": "^0.10.0",
55+
"ansis": "^4.1.0",
5556
"chokidar": "^4.0.1",
5657
"decache": "^4.6.2",
5758
"dot-prop": "9.0.0",
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1-
type logFunction = (...message: unknown[]) => void
1+
import ansis from 'ansis'
2+
3+
type logFunction = (message?: string) => void
24

35
export type Logger = {
46
error: logFunction
57
log: logFunction
68
warn: logFunction
79
}
10+
11+
export const netlifyCommand = ansis.cyanBright
12+
13+
export const netlifyCyan = ansis.rgb(40, 180, 170)
14+
15+
export const netlifyBanner = netlifyCyan('⬥ Netlify')

packages/dev-utils/src/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export { headers, toMultiValueHeaders } from './lib/headers.js'
88
export * as globalConfig from './lib/global-config.js'
99
export { Handler } from './lib/handler.js'
1010
export { LocalState } from './lib/local-state.js'
11-
export type { Logger } from './lib/logger.js'
11+
export { type Logger, netlifyCommand, netlifyCyan, netlifyBanner } from './lib/logger.js'
1212
export { memoize, MemoizeCache } from './lib/memoize.js'
1313
export { HTTPServer } from './server/http_server.js'
1414
export { watchDebounced } from './lib/watch-debounced.js'

packages/dev/src/main.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@ import path from 'node:path'
44
import process from 'node:process'
55

66
import { resolveConfig } from '@netlify/config'
7-
import { ensureNetlifyIgnore, getAPIToken, mockLocation, LocalState, type Logger, HTTPServer } from '@netlify/dev-utils'
7+
import {
8+
ensureNetlifyIgnore,
9+
getAPIToken,
10+
mockLocation,
11+
LocalState,
12+
type Logger,
13+
HTTPServer,
14+
netlifyCommand,
15+
} from '@netlify/dev-utils'
816
import { EdgeFunctionsHandler } from '@netlify/edge-functions/dev'
917
import { FunctionsHandler } from '@netlify/functions/dev'
1018
import { HeadersHandler, type HeadersCollector } from '@netlify/headers'
@@ -251,7 +259,7 @@ export class NetlifyDev {
251259
const { pathname } = new URL(matchRequest.url)
252260
if (pathname.startsWith('/.netlify/images')) {
253261
this.#logger.error(
254-
'The Netlify Image CDN is currently only supported in the Netlify CLI. Run `npx netlify dev` to get started.',
262+
`The Netlify Image CDN is currently only supported in the Netlify CLI. Run ${netlifyCommand('npx netlify dev')} to get started.`,
255263
)
256264

257265
return
@@ -468,4 +476,10 @@ export class NetlifyDev {
468476
async stop() {
469477
await Promise.allSettled(this.#cleanupJobs.map((task) => task()))
470478
}
479+
480+
public getEnabledFeatures(): string[] {
481+
return Object.entries(this.#features)
482+
.filter(([_, enabled]) => enabled)
483+
.map(([feature]) => feature)
484+
}
471485
}

packages/edge-functions/dev/node/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ export class EdgeFunctionsHandler {
288288
} catch (error) {
289289
success = false
290290

291-
this.logger.error('An error occurred while setting up the Netlify Edge Functions environment:', error)
291+
this.logger.error(`An error occurred while setting up the Netlify Edge Functions environment: ${String(error)}`)
292292
}
293293

294294
// The Promise above will resolve as soon as we start the command, but we

packages/vite-plugin/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
},
3838
"dependencies": {
3939
"@netlify/dev": "4.1.1",
40+
"@netlify/dev-utils": "^3.1.0",
4041
"chalk": "^5.4.1"
4142
},
4243
"peerDependencies": {
Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import chalk from 'chalk'
1+
import { type Logger as ViteLogger } from 'vite'
2+
import type { Logger } from '@netlify/dev-utils'
3+
import { netlifyBanner } from '@netlify/dev-utils'
24

3-
const NETLIFY_CYAN = chalk.rgb(40, 180, 170)
4-
const banner = NETLIFY_CYAN('⬥ Netlify ⬥')
5-
6-
export const logger = {
7-
error: (...data: any[]) => (data.length === 0 ? console.error(...data) : console.error(banner, ...data)),
8-
log: (...data: any[]) => (data.length === 0 ? console.log(...data) : console.log(banner, ...data)),
9-
warn: (...data: any[]) => (data.length === 0 ? console.warn(...data) : console.warn(banner, ...data)),
10-
}
5+
export const createLoggerFromViteLogger = (viteLogger: ViteLogger): Logger => ({
6+
error: (msg?: string) => viteLogger.error(msg ?? '', { timestamp: true, environment: netlifyBanner }),
7+
log: (msg?: string) => viteLogger.info(msg ?? '', { timestamp: true, environment: netlifyBanner }),
8+
warn: (msg?: string) => viteLogger.warn(msg ?? '', { timestamp: true, environment: netlifyBanner }),
9+
})

packages/vite-plugin/src/main.test.ts

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url'
55

66
import { Fixture } from '@netlify/dev-utils'
77
import { type Browser, type ConsoleMessage, type Page, chromium } from 'playwright'
8-
import { afterEach, beforeEach, describe, expect, test } from 'vitest'
8+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
99
import { createServer } from 'vite'
1010

1111
import netlify from './main.js'
@@ -43,6 +43,18 @@ const startTestServer = async (options: Parameters<typeof createServer>[0] = {})
4343
return { server, url: `http://localhost:${port.toString()}` }
4444
}
4545

46+
const createMockViteLogger = () => {
47+
return {
48+
info: vi.fn(),
49+
warn: vi.fn(),
50+
warnOnce: vi.fn(),
51+
error: vi.fn(),
52+
clearScreen: vi.fn(),
53+
hasErrorLogged: vi.fn(),
54+
hasWarned: false,
55+
}
56+
}
57+
4658
describe('Plugin constructor', () => {
4759
test('Is a no-op when running in the Netlify CLI', () => {
4860
process.env.NETLIFY_DEV = 'true'
@@ -98,6 +110,58 @@ describe('configureServer', { timeout: 15_000 }, () => {
98110
await fixture.destroy()
99111
})
100112

113+
test('Prints a basic message on server start', async () => {
114+
const fixture = new Fixture()
115+
.withFile(
116+
'vite.config.js',
117+
`import { defineConfig } from 'vite';
118+
import netlify from '@netlify/vite-plugin';
119+
120+
export default defineConfig({
121+
plugins: [
122+
netlify({ middleware: false })
123+
]
124+
});`,
125+
)
126+
.withFile(
127+
'index.html',
128+
`<!DOCTYPE html>
129+
<html>
130+
<head><title>Hello World</title></head>
131+
<body><h1>Hello from the browser</h1></body>
132+
</html>`,
133+
)
134+
const directory = await fixture.create()
135+
await fixture
136+
.withPackages({
137+
vite: '6.0.0',
138+
'@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(),
139+
})
140+
.create()
141+
142+
const mockLogger = createMockViteLogger()
143+
const { server } = await startTestServer({
144+
root: directory,
145+
logLevel: 'info',
146+
customLogger: mockLogger,
147+
})
148+
149+
expect(mockLogger.error).not.toHaveBeenCalled()
150+
expect(mockLogger.warn).not.toHaveBeenCalled()
151+
expect(mockLogger.warnOnce).not.toHaveBeenCalled()
152+
expect(mockLogger.info).toHaveBeenCalledTimes(2)
153+
expect(mockLogger.info).toHaveBeenNthCalledWith(1, 'Environment loaded', expect.objectContaining({}))
154+
expect(mockLogger.info).toHaveBeenNthCalledWith(
155+
2,
156+
'💭 Linking this project to a Netlify site lets you deploy your site, use any environment variables \
157+
defined on your team and site and much more. Run npx netlify init to get started.',
158+
expect.objectContaining({}),
159+
)
160+
161+
await server.close()
162+
await fixture.destroy()
163+
})
164+
101165
describe('Middleware enabled', () => {
102166
let browser: Browser
103167
let page: Page
@@ -111,6 +175,65 @@ describe('configureServer', { timeout: 15_000 }, () => {
111175
await browser.close()
112176
})
113177

178+
test('Prints a message listing emulated features on server start', async () => {
179+
const fixture = new Fixture()
180+
.withFile(
181+
'vite.config.js',
182+
`import { defineConfig } from 'vite';
183+
import netlify from '@netlify/vite-plugin';
184+
185+
export default defineConfig({
186+
plugins: [
187+
netlify({
188+
middleware: true,
189+
edgeFunctions: { enabled: false },
190+
})
191+
]
192+
});`,
193+
)
194+
.withFile(
195+
'index.html',
196+
`<!DOCTYPE html>
197+
<html>
198+
<head><title>Hello World</title></head>
199+
<body><h1>Hello from the browser</h1></body>
200+
</html>`,
201+
)
202+
const directory = await fixture.create()
203+
await fixture
204+
.withPackages({
205+
vite: '6.0.0',
206+
'@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(),
207+
})
208+
.create()
209+
210+
const mockLogger = createMockViteLogger()
211+
const { server } = await startTestServer({
212+
root: directory,
213+
logLevel: 'info',
214+
customLogger: mockLogger,
215+
})
216+
217+
expect(mockLogger.error).not.toHaveBeenCalled()
218+
expect(mockLogger.warn).not.toHaveBeenCalled()
219+
expect(mockLogger.warnOnce).not.toHaveBeenCalled()
220+
expect(mockLogger.info).toHaveBeenCalledTimes(3)
221+
expect(mockLogger.info).toHaveBeenNthCalledWith(1, 'Environment loaded', expect.objectContaining({}))
222+
expect(mockLogger.info).toHaveBeenNthCalledWith(
223+
2,
224+
'Middleware loaded. Emulating features: blobs, environmentVariables, functions, headers, redirects, static.',
225+
expect.objectContaining({}),
226+
)
227+
expect(mockLogger.info).toHaveBeenNthCalledWith(
228+
3,
229+
expect.stringContaining('Linking this project to a Netlify site'),
230+
expect.objectContaining({}),
231+
)
232+
233+
await server.close()
234+
await fixture.destroy()
235+
})
236+
114237
test('Returns static files from project dir', async () => {
115238
const fixture = new Fixture()
116239
.withFile(

packages/vite-plugin/src/main.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import process from 'node:process'
22

33
import { NetlifyDev, type Features } from '@netlify/dev'
4+
import { netlifyCommand } from '@netlify/dev-utils'
45
import * as vite from 'vite'
56

6-
import { logger } from './lib/logger.js'
7+
import { createLoggerFromViteLogger } from './lib/logger.js'
78
import { fromWebResponse } from './lib/reqres.js'
89

910
export interface NetlifyPluginOptions extends Features {
@@ -24,6 +25,7 @@ export default function netlify(options: NetlifyPluginOptions = {}): any {
2425
const plugin: vite.Plugin = {
2526
name: 'vite-plugin-netlify',
2627
async configureServer(viteDevServer) {
28+
const logger = createLoggerFromViteLogger(viteDevServer.config.logger)
2729
const { port } = viteDevServer.config.server
2830
const { blobs, edgeFunctions, functions, middleware = true, redirects, staticFiles } = options
2931
const netlifyDev = new NetlifyDev({
@@ -41,12 +43,7 @@ export default function netlify(options: NetlifyPluginOptions = {}): any {
4143
})
4244

4345
await netlifyDev.start()
44-
45-
if (!netlifyDev.siteIsLinked) {
46-
logger.log(
47-
'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.',
48-
)
49-
}
46+
logger.log('Environment loaded')
5047

5148
if (middleware) {
5249
viteDevServer.middlewares.use(async function netlifyPreMiddleware(nodeReq, nodeRes, next) {
@@ -72,6 +69,13 @@ export default function netlify(options: NetlifyPluginOptions = {}): any {
7269

7370
next()
7471
})
72+
logger.log(`Middleware loaded. Emulating features: ${netlifyDev.getEnabledFeatures().join(', ')}.`)
73+
}
74+
75+
if (!netlifyDev.siteIsLinked) {
76+
logger.log(
77+
`💭 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.`,
78+
)
7579
}
7680
},
7781
}

0 commit comments

Comments
 (0)