diff --git a/packages/playground/cli/__tests__/cli.spec.ts b/packages/playground/cli/__tests__/cli.spec.ts new file mode 100644 index 00000000000000..3b735faef95ddf --- /dev/null +++ b/packages/playground/cli/__tests__/cli.spec.ts @@ -0,0 +1,18 @@ +import { port } from './serve' + +test('cli should work', async () => { + // this test uses a custom serve implementation, so regular helpers for browserLogs and goto don't work + // do the same thing manually + const logs = [] + const onConsole = (msg) => { + logs.push(msg.text()) + } + try { + page.on('console', onConsole) + await page.goto(`http://localhost:${port}/`) + expect(await page.textContent('.app')).toBe('vite cli works!') + expect(logs.some((msg) => msg.match('vite cli works!'))).toBe(true) + } finally { + page.off('console', onConsole) + } +}) diff --git a/packages/playground/cli/__tests__/serve.js b/packages/playground/cli/__tests__/serve.js new file mode 100644 index 00000000000000..5dd058f4e1a83c --- /dev/null +++ b/packages/playground/cli/__tests__/serve.js @@ -0,0 +1,166 @@ +// @ts-check +// this is automtically detected by scripts/jestPerTestSetup.ts and will replace +// the default e2e test serve behavior + +const path = require('path') +// eslint-disable-next-line node/no-restricted-require +const execa = require('execa') +const { workspaceRoot } = require('../../testUtils') + +const isWindows = process.platform === 'win32' +const port = (exports.port = 9510) // make sure this port is unique across tests with custom servers +const viteBin = path.join(workspaceRoot, 'packages', 'vite', 'bin', 'vite.js') + +/** + * @param {string} root + * @param {boolean} isProd + */ +exports.serve = async function serve(root, isProd) { + // collect stdout and stderr streams from child processes here to avoid interfering with regular jest output + const streams = { + build: { out: [], err: [] }, + server: { out: [], err: [] } + } + // helpers to collect streams + const collectStreams = (name, process) => { + process.stdout.on('data', (d) => streams[name].out.push(d.toString())) + process.stderr.on('data', (d) => streams[name].err.push(d.toString())) + } + const collectErrorStreams = (name, e) => { + e.stdout && streams[name].out.push(e.stdout) + e.stderr && streams[name].err.push(e.stderr) + } + + // helper to output stream content on error + const printStreamsToConsole = async (name) => { + const std = streams[name] + if (std.out && std.out.length > 0) { + console.log(`stdout of ${name}\n${std.out.join('\n')}\n`) + } + if (std.err && std.err.length > 0) { + console.log(`stderr of ${name}\n${std.err.join('\n')}\n`) + } + } + + // only run `vite build` when needed + if (isProd) { + const buildCommand = `${viteBin} build` + try { + const buildProcess = execa.command(buildCommand, { + cwd: root, + stdio: 'pipe' + }) + collectStreams('build', buildProcess) + await buildProcess + } catch (e) { + console.error(`error while executing cli command "${buildCommand}":`, e) + collectErrorStreams('build', e) + await printStreamsToConsole('build') + throw e + } + } + + // run `vite --port x` or `vite preview --port x` to start server + const viteServerArgs = ['--port', `${port}`, '--strict-port'] + if (isProd) { + viteServerArgs.unshift('preview') + } + const serverCommand = `${viteBin} ${viteServerArgs.join(' ')}` + const serverProcess = execa.command(serverCommand, { + cwd: root, + stdio: 'pipe' + }) + collectStreams('server', serverProcess) + + // close server helper, send SIGTERM followed by SIGKILL if needed, give up after 3sec + const close = async () => { + if (serverProcess) { + const timeoutError = `server process still alive after 3s` + try { + killProcess(serverProcess) + await resolvedOrTimeout(serverProcess, 3000, timeoutError) + } catch (e) { + if (e === timeoutError || (!serverProcess.killed && !isWindows)) { + collectErrorStreams('server', e) + console.error( + `error while killing cli command "${serverCommand}":`, + e + ) + await printStreamsToConsole('server') + } + } + } + } + + try { + await startedOnPort(serverProcess, port, 3000) + return { close } + } catch (e) { + collectErrorStreams('server', e) + console.error(`error while executing cli command "${serverCommand}":`, e) + await printStreamsToConsole('server') + try { + await close() + } catch (e1) { + console.error( + `error while killing cli command after failed execute "${serverCommand}":`, + e1 + ) + } + } +} + +// helper to validate that server was started on the correct port +async function startedOnPort(serverProcess, port, timeout) { + let checkPort + const startedPromise = new Promise((resolve, reject) => { + checkPort = (data) => { + const str = data.toString() + // hack, console output may contain color code gibberish + // skip gibberish between localhost: and port number + const match = str.match(/(http:\/\/localhost:)(?:.*)(\d{4})/) + if (match) { + const startedPort = parseInt(match[2], 10) + if (startedPort === port) { + resolve() + } else { + const msg = `server listens on port ${startedPort} instead of ${port}` + reject(msg) + } + } + } + serverProcess.stdout.on('data', checkPort) + }) + return resolvedOrTimeout( + startedPromise, + timeout, + `failed to start within ${timeout}ms` + ).finally(() => serverProcess.stdout.off('data', checkPort)) +} + +// helper function to kill process, uses taskkill on windows to ensure child process is killed too +function killProcess(serverProcess) { + if (isWindows) { + try { + execa.commandSync(`taskkill /pid ${serverProcess.pid} /T /F`) + } catch (e) { + console.error('failed to taskkill:', e) + } + } else { + serverProcess.kill('SIGTERM', { forceKillAfterTimeout: 2000 }) + } +} + +// helper function that rejects with errorMessage if promise isn't settled within ms +async function resolvedOrTimeout(promise, ms, errorMessage) { + let timer + return Promise.race([ + promise, + new Promise((_, reject) => { + timer = setTimeout(() => reject(errorMessage), ms) + }) + ]).finally(() => { + clearTimeout(timer) + timer = null + }) +} diff --git a/packages/playground/cli/index.html b/packages/playground/cli/index.html new file mode 100644 index 00000000000000..9dabaa9ddeb84b --- /dev/null +++ b/packages/playground/cli/index.html @@ -0,0 +1,3 @@ + + +
vite cli works!
diff --git a/packages/playground/cli/index.js b/packages/playground/cli/index.js new file mode 100644 index 00000000000000..6b6158ca27aaf8 --- /dev/null +++ b/packages/playground/cli/index.js @@ -0,0 +1 @@ +console.log('vite cli works!') diff --git a/packages/playground/cli/package.json b/packages/playground/cli/package.json new file mode 100644 index 00000000000000..0518f1fd930210 --- /dev/null +++ b/packages/playground/cli/package.json @@ -0,0 +1,11 @@ +{ + "name": "test-cli", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "debug": "node --inspect-brk ../../vite/bin/vite", + "serve": "vite preview" + } +} diff --git a/packages/playground/cli/vite.config.js b/packages/playground/cli/vite.config.js new file mode 100644 index 00000000000000..014aa6d378934b --- /dev/null +++ b/packages/playground/cli/vite.config.js @@ -0,0 +1,12 @@ +const { defineConfig } = require('vite') + +module.exports = defineConfig({ + server: { + host: 'localhost' + }, + build: { + //speed up build + minify: false, + target: 'esnext' + } +}) diff --git a/packages/playground/css/postcss-caching/css.spec.ts b/packages/playground/css/postcss-caching/css.spec.ts index 835538666db7bb..6c85d127003680 100644 --- a/packages/playground/css/postcss-caching/css.spec.ts +++ b/packages/playground/css/postcss-caching/css.spec.ts @@ -4,26 +4,51 @@ import path from 'path' test('postcss config', async () => { const port = 5005 + const startServer = async (root) => { + const server = await createServer({ + root, + logLevel: 'silent', + server: { + port, + strictPort: true + }, + build: { + // skip transpilation during tests to make it faster + target: 'esnext' + } + }) + await server.listen() + return server + } const blueAppDir = path.join(__dirname, 'blue-app') const greenAppDir = path.join(__dirname, 'green-app') + let blueApp + let greenApp + try { + blueApp = await startServer(blueAppDir) - process.chdir(blueAppDir) - const blueApp = await createServer() - await blueApp.listen(port) - await page.goto(`http://localhost:${port}`) - const blueA = await page.$('.postcss-a') - expect(await getColor(blueA)).toBe('blue') - const blueB = await page.$('.postcss-b') - expect(await getColor(blueB)).toBe('black') - await blueApp.close() + await page.goto(`http://localhost:${port}`) + const blueA = await page.$('.postcss-a') + expect(await getColor(blueA)).toBe('blue') + const blueB = await page.$('.postcss-b') + expect(await getColor(blueB)).toBe('black') + await blueApp.close() + blueApp = null - process.chdir(greenAppDir) - const greenApp = await createServer() - await greenApp.listen(port) - await page.goto(`http://localhost:${port}`) - const greenA = await page.$('.postcss-a') - expect(await getColor(greenA)).toBe('black') - const greenB = await page.$('.postcss-b') - expect(await getColor(greenB)).toBe('green') - await greenApp.close() + greenApp = await startServer(greenAppDir) + await page.goto(`http://localhost:${port}`) + const greenA = await page.$('.postcss-a') + expect(await getColor(greenA)).toBe('black') + const greenB = await page.$('.postcss-b') + expect(await getColor(greenB)).toBe('green') + await greenApp.close() + greenApp = null + } finally { + if (blueApp) { + await blueApp.close() + } + if (greenApp) { + await greenApp.close() + } + } }) diff --git a/packages/playground/testUtils.ts b/packages/playground/testUtils.ts index f0b54f1e5c972d..2be13b1aee8cc4 100644 --- a/packages/playground/testUtils.ts +++ b/packages/playground/testUtils.ts @@ -17,6 +17,7 @@ export const isBuild = !!process.env.VITE_TEST_BUILD const testPath = expect.getState().testPath const testName = slash(testPath).match(/playground\/([\w-]+)\//)?.[1] export const testDir = path.resolve(__dirname, '../../packages/temp', testName) +export const workspaceRoot = path.resolve(__dirname, '../../') const hexToNameMap: Record = {} Object.keys(colors).forEach((color) => { diff --git a/packages/vite/src/node/cli.ts b/packages/vite/src/node/cli.ts index 605c274cba2e58..1166b48f35f857 100644 --- a/packages/vite/src/node/cli.ts +++ b/packages/vite/src/node/cli.ts @@ -106,19 +106,16 @@ cli throw new Error('HTTP server not available') } + await server.listen() + printHttpServerUrls(server.httpServer, server.config, options) // @ts-ignore if (global.__vite_start_time) { - info( - chalk.cyan( - // @ts-ignore - performance.now() - global.__vite_start_time - ) - ) + // @ts-ignore + const startupDuration = performance.now() - global.__vite_start_time + info(`\n ${chalk.cyan(`ready in ${Math.ceil(startupDuration)}ms.`)}\n`) } - - await server.listen() } catch (e) { createLogger(options.logLevel).error( chalk.red(`error when starting dev server:\n${e.stack}`), diff --git a/packages/vite/src/node/logger.ts b/packages/vite/src/node/logger.ts index a6a6ae279f71e2..5ba5adf7c63054 100644 --- a/packages/vite/src/node/logger.ts +++ b/packages/vite/src/node/logger.ts @@ -160,7 +160,7 @@ export function printHttpServerUrls( } } -export function printServerUrls( +function printServerUrls( hostname: Hostname, protocol: string, port: number, @@ -176,7 +176,7 @@ export function printServerUrls( } else { Object.values(os.networkInterfaces()) .flatMap((nInterface) => nInterface ?? []) - .filter((detail) => detail.family === 'IPv4') + .filter((detail) => detail && detail.address && detail.family === 'IPv4') .map((detail) => { const type = detail.address.includes('127.0.0.1') ? 'Local: ' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 325d637a43700f..5a3488b3a4033a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -120,6 +120,9 @@ importers: dependencies: tailwindcss: 2.2.15_ts-node@10.2.1 + packages/playground/cli: + specifiers: {} + packages/playground/css: specifiers: css-dep: link:./css-dep