diff --git a/bun.lockb b/bun.lockb index 7f4e8e0..ae65ccc 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index edf5f97..97908eb 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ }, "devDependencies": { "@stacksjs/eslint-config": "^3.8.1-beta.2", - "@stacksjs/rpx": "^0.6.5", + "@stacksjs/rpx": "^0.7.1", "@types/bun": "^1.1.14", "bumpp": "^9.9.0", "bun-plugin-dtsx": "^0.21.9", diff --git a/src/index.ts b/src/index.ts index a9c2f1b..3592302 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,45 +6,78 @@ import { readFileSync } from 'node:fs' import { join } from 'node:path' import process from 'node:process' import { promisify } from 'node:util' -import { cleanup, startProxies } from '@stacksjs/rpx' -import colors from 'picocolors' +import { checkExistingCertificates, checkHosts, cleanup, startProxies } from '@stacksjs/rpx' +import colors, { dim } from 'picocolors' import packageJson from '../package.json' import { buildConfig } from './utils' function getPackageVersions() { let viteVersion + const vitePluginLocalVersion = packageJson.version + let vitePressVersion + try { - // Try to get Vite version from node_modules first + // Try to get VitePress version first + const vitePressPath = join(process.cwd(), 'node_modules', 'vitepress', 'package.json') + try { + const vitePressPackage = JSON.parse(readFileSync(vitePressPath, 'utf-8')) + vitePressVersion = vitePressPackage.version + } + catch { } + + // Get Vite version const vitePackageJson = readFileSync( join(process.cwd(), 'node_modules', 'vite', 'package.json'), 'utf-8', ) viteVersion = JSON.parse(vitePackageJson).version } - // eslint-disable-next-line unused-imports/no-unused-vars catch (error) { // Fallback to package.json dependencies - viteVersion = packageJson.devDependencies?.vite?.replace('^', '') - || '0.0.0' + viteVersion = packageJson.devDependencies?.vite?.replace('^', '') || '0.0.0' } return { + 'vitepress': vitePressVersion, 'vite': viteVersion, - 'vite-plugin-local': packageJson.version, + 'vite-plugin-local': vitePluginLocalVersion, } } const execAsync = promisify(exec) -// Simple sudo validation -async function validateSudo(): Promise { +async function needsSudoAccess(options: VitePluginLocalOptions, domain: string): Promise { try { - await execAsync('sudo -n true') - return true + // Check if we need to generate certificates + if (options.https) { + const config = buildConfig(options, 'localhost:5173') // temporary URL for config + const existingCerts = await checkExistingCertificates(config) + if (!existingCerts) { + return true + } + } + + // Check if we need to modify hosts file + if (!domain.includes('localhost') && !domain.includes('127.0.0.1')) { + const hostsExist = await checkHosts([domain], options.verbose) + // Only need sudo if hosts don't exist and we don't have write permission + if (!hostsExist[0]) { + try { + // Try to write a test file to check permissions + await execAsync('touch /etc/hosts', { stdio: 'ignore' }) + return false + } + catch { + return true + } + } + } + + return false } - // eslint-disable-next-line unused-imports/no-unused-vars catch (error) { - return false + console.error('Error checking sudo requirements:', error) + return false // Changed to false - if we can't check, don't assume we need sudo } } @@ -54,33 +87,21 @@ export function VitePluginLocal(options: VitePluginLocalOptions): Plugin { verbose = false, etcHostsCleanup = true, } = options - let domains: string[] | undefined let proxyUrl: string | undefined let originalConsole: typeof console let cleanupPromise: Promise | null = null + let isCleaningUp = false const debug = (...args: any[]) => { if (verbose) originalConsole.log('[vite-plugin-local]', ...args) } - // Override the library's process.exit - const originalExit = process.exit - process.exit = ((code?: number) => { - if (cleanupPromise) { - cleanupPromise.finally(() => { - process.exit = originalExit - process.exit(code) - }) - return undefined as never - } - return originalExit(code) - }) as (code?: number) => never - // Add cleanup handler for process exit const exitHandler = async () => { - if (domains?.length) { + if (domains?.length && !isCleaningUp) { + isCleaningUp = true debug('Cleaning up...') cleanupPromise = cleanup({ domains, @@ -90,9 +111,23 @@ export function VitePluginLocal(options: VitePluginLocalOptions): Plugin { await cleanupPromise domains = undefined debug('Cleanup complete') + isCleaningUp = false } } + // Override the library's process.exit + const originalExit = process.exit + process.exit = ((code?: number) => { + if (cleanupPromise || domains?.length) { + exitHandler().finally(() => { + process.exit = originalExit + process.exit(code) + }) + return undefined as never + } + return originalExit(code) + }) as (code?: number) => never + // Handle cleanup for different termination signals process.on('SIGINT', exitHandler) process.on('SIGTERM', exitHandler) @@ -102,124 +137,125 @@ export function VitePluginLocal(options: VitePluginLocalOptions): Plugin { name: 'vite-plugin-local', enforce: 'pre', - config(config) { - if (!enabled) - return config - - config.server = config.server || {} - config.server.middlewareMode = false - - return config - }, - - async configureServer(server: ViteDevServer) { + configureServer(server: ViteDevServer) { if (!enabled) return - try { - originalConsole = { ...console } - - debug('Checking sudo access...') - const hasSudo = await validateSudo() - - if (!hasSudo) { - console.log('\nSudo access required for proxy setup.') - console.log('Please enter your password when prompted.\n') - - try { - await execAsync('sudo true') - } - // eslint-disable-next-line unused-imports/no-unused-vars - catch (error) { - console.error('Failed to get sudo access. Please try again.') - process.exit(1) - } + // Override console.log immediately to prevent VitePress initial messages + const originalLog = console.log + console.log = (...args) => { + if (typeof args[0] === 'string' && ( + args[0].includes('vitepress v') + || args[0].includes('press h to show help') + )) { + return } + originalLog.apply(console, args) + } - debug('Sudo access validated') + // Store original console for debug + originalConsole = { ...console } - const setupProxy = async () => { - try { - const host = typeof server.config.server.host === 'boolean' - ? 'localhost' - : server.config.server.host || 'localhost' + server.printUrls = () => { } - const port = server.config.server.port || 5173 - const serverUrl = `${host}:${port}` + const setupPlugin = async () => { + try { + const config = buildConfig(options, 'localhost:5173') + const domain = config.to - const config = buildConfig(options, serverUrl) - domains = [config.to] - proxyUrl = config.to + // Check if we need sudo + const needsSudo = await needsSudoAccess(options, domain) - // Suppress all console output during proxy setup - const noOp = () => { } - console.log = noOp - console.info = noOp - console.warn = noOp + if (needsSudo) { + debug('Sudo access required') + // Only show sudo message if we actually need it + console.log('\nSudo access required for proxy setup.') + console.log('Please enter your password when prompted.\n') - debug('Starting proxies...') - await startProxies(config) + try { + await execAsync('sudo true') + } + catch (error) { + console.error('Failed to get sudo access. Please try again.') + process.exit(1) + } + } - // Restore console - console.log = originalConsole.log - console.info = originalConsole.info - console.warn = originalConsole.warn + const setupProxy = async () => { + try { + const host = typeof server.config.server.host === 'boolean' + ? 'localhost' + : server.config.server.host || 'localhost' - // Custom print URLs function - server.printUrls = function () { - const protocol = options.https ? 'https' : 'http' const port = server.config.server.port || 5173 - const localUrl = `http://localhost:${port}/` - const proxiedUrl = `${protocol}://${proxyUrl}/` - - const colorUrl = (url: string) => - colors.cyan(url.replace(/:(\d+)\//, (_, port) => `:${colors.bold(port)}/`)) - - const versions = getPackageVersions() - - console.log( - `\n${colors.bold(colors.green('vite'))} ${colors.green(`v${versions.vite}`)} ${colors.italic( - colors.green('&'), - )} ${colors.bold(colors.green('vite-plugin-local'))} ${colors.green(`v${versions['vite-plugin-local']}`)}\n`, - ) - - console.log(` ${colors.green('➜')} ${colors.bold('Local')}: ${colorUrl(localUrl)}`) - console.log(` ${colors.green('➜')} ${colors.bold('Proxied')}: ${colorUrl(proxiedUrl)}`) - - if (options.https) { - console.log(` ${colors.green('➜')} ${colors.bold('SSL')}: ${colors.dim('TLS 1.2/1.3, HTTP/2')}`) + const serverUrl = `${host}:${port}` + + const config = buildConfig(options, serverUrl) + domains = [config.to] + proxyUrl = config.to + + debug('Starting proxies...') + + await startProxies(config) + + server.printUrls = function () { + const protocol = options.https ? 'https' : 'http' + const port = server.config.server.port || 5173 + const localUrl = `http://localhost:${port}/` + const proxiedUrl = `${protocol}://${proxyUrl}/` + const colorUrl = (url: string) => + colors.cyan(url.replace(/:(\d+)\//, (_, port) => `:${colors.bold(port)}/`)) + + const versions = getPackageVersions() + if (versions.vitepress) { + console.log( + `\n ${colors.bold(colors.green('vitepress'))} ${colors.green(`v${versions.vitepress}`)} via ${colors.bold(colors.green('vite-plugin-local'))} ${colors.green(`v${versions['vite-plugin-local']}`)}\n`, + ) + } + else { + console.log( + `\n ${colors.bold(colors.green('vite'))} ${colors.green(`v${versions.vite}`)} via ${colors.bold(colors.green('vite-plugin-local'))} ${colors.green(`v${versions['vite-plugin-local']}`)}\n`, + ) + } + + console.log(` ${colors.green('➜')} ${colors.bold('Local')}: ${colorUrl(localUrl)}`) + console.log(` ${colors.green('➜')} ${colors.bold('Proxied')}: ${colorUrl(proxiedUrl)}`) + + if (options.https) { + console.log(` ${colors.green('➜')} ${colors.bold('SSL')}: ${colors.dim('TLS 1.2/1.3, HTTP/2')}`) + } + + console.log( + colors.dim(` ${colors.green('➜')} ${colors.bold('Network')}: use `) + + colors.bold('--host') + + colors.dim(' to expose'), + ) + + console.log(`\n ${colors.green('➜')} ${dim('press')} ${colors.bold('h')} ${dim('to show help')}\n`) } - console.log( - colors.dim(` ${colors.green('➜')} ${colors.bold('Network')}: use `) - + colors.bold('--host') - + colors.dim(' to expose'), - ) - - console.log(`\n ${colors.green('➜')} press ${colors.bold('h')} to show help\n`) + server.printUrls() + debug('Proxy setup complete') + } + catch (error) { + console.error('Failed to start reverse proxy:', error) + process.exit(1) } - - server.printUrls() - debug('Proxy setup complete') } - catch (error) { - console.error('Failed to start reverse proxy:', error) + + server.httpServer?.once('listening', setupProxy) + if (server.httpServer?.listening) { + debug('Server already listening, setting up proxy immediately') + await setupProxy() } } - - server.httpServer?.once('listening', setupProxy) - if (server.httpServer?.listening) { - debug('Server already listening, setting up proxy immediately') - await setupProxy() + catch (error) { + console.error('Failed to initialize plugin:', error) + process.exit(1) } } - catch (error) { - console.error('Failed to initialize plugin:', error) - } - server.httpServer?.once('close', async () => { - await exitHandler() - }) + return setupPlugin() }, } }