From 8482dcbf56d069c85b707691de2345d8b6dad7ae Mon Sep 17 00:00:00 2001 From: Rihan Arfan Date: Thu, 11 Sep 2025 22:44:07 +0100 Subject: [PATCH 1/2] feat: database viewer --- package.json | 2 + pnpm-lock.yaml | 6 ++ src/utils/devtools.ts | 149 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 147 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 8b414865..13dd056b 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,8 @@ "db0": "^0.3.2", "defu": "^6.1.4", "destr": "^2.0.5", + "execa": "^9.6.0", + "get-port-please": "^3.2.0", "h3": "^1.15.4", "mime": "^4.0.7", "nitro-cloudflare-dev": "^0.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 51a69696..dcd127c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,12 @@ importers: destr: specifier: ^2.0.5 version: 2.0.5 + execa: + specifier: ^9.6.0 + version: 9.6.0 + get-port-please: + specifier: ^3.2.0 + version: 3.2.0 h3: specifier: ^1.15.4 version: 1.15.4 diff --git a/src/utils/devtools.ts b/src/utils/devtools.ts index e4366e12..1f11b9c7 100644 --- a/src/utils/devtools.ts +++ b/src/utils/devtools.ts @@ -1,18 +1,147 @@ import { addCustomTab } from '@nuxt/devtools-kit' +import { logger } from '@nuxt/kit' import type { Nuxt } from 'nuxt/schema' import type { HubConfig } from '../features' +import { getPort, waitForPort } from 'get-port-please' +import { existsSync } from 'node:fs' +import { writeFile } from 'node:fs/promises' +import { detectPackageManager, dlxCommand } from 'nypm' +import { execa } from 'execa' +import { join } from 'pathe' -export function addDevToolsCustomTabs(nuxt: Nuxt, _hub: HubConfig) { - nuxt.hook('listen', (_) => { - nuxt.options.nitro.experimental?.openAPI && addCustomTab({ - category: 'server', - name: 'hub-open-api', - title: 'OpenAPI', - icon: 'i-lucide-file-text', - view: { - type: 'iframe', - src: `/_scalar` +let isReady = false +let promise: Promise | null = null +let port = 4983 + +const log = logger.withTag('nuxt:hub') + +async function launchDrizzleStudio(nuxt: Nuxt) { + const packageManager = await detectPackageManager(nuxt.options.rootDir) + if (!packageManager) { + throw new Error('Could not detect package manager') + } + + port = await getPort({ port: 4983 }) + let cmd = `${packageManager.name} run drizzle-kit studio --port ${port}` + + try { + // Check if there's a drizzle.config.ts in the project root + const drizzleConfigPath = join(nuxt.options.rootDir, 'drizzle.config.ts') + const drizzleConfigNuxtPath = join(nuxt.options.buildDir, 'drizzle.config.ts') + const drizzleConfigExists = existsSync(drizzleConfigPath) + + let configPath = drizzleConfigPath + + // If no drizzle.config.ts exists, create one using nitro database config + if (!drizzleConfigExists) { + const dbConfig = nuxt.options.nitro.devDatabase?.db + if (!dbConfig?.connector) { + throw new Error('No database configuration found. Please configure your database in nuxt.config.ts') } + + // Determine dialect from connector + let dialect: string + let dbCredentials: any + + if (dbConfig.connector === 'postgresql' || dbConfig.connector === 'pglite') { + dialect = 'postgresql' + if (dbConfig.connector === 'pglite') { + dbCredentials = { + url: dbConfig.options?.dataDir || './database/' + } + } else { + dbCredentials = { + url: dbConfig.options?.url + } + } + } else if (['better-sqlite3', 'bun-sqlite', 'bun', 'node-sqlite', 'sqlite3'].includes(dbConfig.connector)) { + dialect = 'sqlite' + dbCredentials = { + url: dbConfig.options?.path + } + } else if (dbConfig.connector === 'mysql2') { + dialect = 'mysql' + dbCredentials = { + url: dbConfig.options?.url || process.env.DATABASE_URL + } + } else { + throw new Error(`Unsupported database connector: ${dbConfig.connector}`) + } + + // Generate drizzle config content + const drizzleConfig = `import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + dialect: "${dialect}"${dbConfig.connector === 'pglite' ? ',\n driver: "pglite"' : ''}, + dbCredentials: ${JSON.stringify(dbCredentials, null, 2)} +});` + await writeFile(drizzleConfigNuxtPath, drizzleConfig, 'utf-8') + configPath = join(nuxt.options.buildDir, 'drizzle.config.ts') + + cmd = dlxCommand(packageManager.name, 'drizzle-kit', { + args: [ + 'studio', + '--config', configPath, + '--port', port.toString() + ], + packages: [dbConfig.connector, 'drizzle-orm', 'drizzle-kit'] + }) + } + + // Launch Drizzle Studio + log.info(`Launching Drizzle Studio...`) + + execa(cmd, { + cwd: nuxt.options.rootDir, + stdio: 'inherit', + shell: true }) + + // await waitForPort(port, { + // delay: 500, + // retries: 30 + // }) + + isReady = true + } catch (error) { + log.error('Failed to launch Drizzle Studio:', error) + throw error + } +} + +export function addDevToolsCustomTabs(nuxt: Nuxt, hub: HubConfig) { + nuxt.options.nitro.experimental?.openAPI && addCustomTab({ + category: 'server', + name: 'hub-open-api', + title: 'OpenAPI', + icon: 'i-lucide-file-text', + view: { + type: 'iframe', + src: `/_scalar` + } + }) + + hub.database && addCustomTab({ + category: 'server', + name: 'hub-database', + title: 'Database', + icon: 'i-lucide-database', + view: isReady && port + ? { + type: 'iframe', + src: `https://local.drizzle.studio?port=${port}` + } + : { + type: 'launch', + description: 'Launch Drizzle Studio', + actions: [{ + label: 'Start', + async handle() { + if (!promise) + promise = launchDrizzleStudio(nuxt) + await promise + } + }] + } }) } From b7034fd53f3b154a4c5ed2fde716fab718e9204f Mon Sep 17 00:00:00 2001 From: Rihan Arfan Date: Fri, 12 Sep 2025 15:06:17 +0100 Subject: [PATCH 2/2] fix: lazy launching --- src/utils/devtools.ts | 85 +++++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/src/utils/devtools.ts b/src/utils/devtools.ts index 1f11b9c7..abb10d5d 100644 --- a/src/utils/devtools.ts +++ b/src/utils/devtools.ts @@ -1,8 +1,7 @@ -import { addCustomTab } from '@nuxt/devtools-kit' import { logger } from '@nuxt/kit' import type { Nuxt } from 'nuxt/schema' import type { HubConfig } from '../features' -import { getPort, waitForPort } from 'get-port-please' +import { checkPort, getPort } from 'get-port-please' import { existsSync } from 'node:fs' import { writeFile } from 'node:fs/promises' import { detectPackageManager, dlxCommand } from 'nypm' @@ -97,12 +96,16 @@ export default defineConfig({ shell: true }) - // await waitForPort(port, { - // delay: 500, - // retries: 30 - // }) - - isReady = true + // Wait for Drizzle Studio to be ready + const checkInterval = 100 // 100ms + while (!isReady) { + const portCheck = await checkPort(port) + if (portCheck !== false) { + isReady = true + break + } + await new Promise(resolve => setTimeout(resolve, checkInterval)) + } } catch (error) { log.error('Failed to launch Drizzle Studio:', error) throw error @@ -110,38 +113,40 @@ export default defineConfig({ } export function addDevToolsCustomTabs(nuxt: Nuxt, hub: HubConfig) { - nuxt.options.nitro.experimental?.openAPI && addCustomTab({ - category: 'server', - name: 'hub-open-api', - title: 'OpenAPI', - icon: 'i-lucide-file-text', - view: { - type: 'iframe', - src: `/_scalar` - } - }) + nuxt.hook('devtools:customTabs', (tabs) => { + if (nuxt.options.nitro.experimental?.openAPI)({ + category: 'server', + name: 'hub-open-api', + title: 'OpenAPI', + icon: 'i-lucide-file-text', + view: { + type: 'iframe', + src: `/_scalar` + } + }) - hub.database && addCustomTab({ - category: 'server', - name: 'hub-database', - title: 'Database', - icon: 'i-lucide-database', - view: isReady && port - ? { - type: 'iframe', - src: `https://local.drizzle.studio?port=${port}` - } - : { - type: 'launch', - description: 'Launch Drizzle Studio', - actions: [{ - label: 'Start', - async handle() { - if (!promise) - promise = launchDrizzleStudio(nuxt) - await promise - } - }] - } + if (hub.database) tabs.push({ + category: 'server', + name: 'hub-database', + title: 'Database', + icon: 'i-lucide-database', + view: isReady && port + ? { + type: 'iframe', + src: `https://local.drizzle.studio?port=${port}` + } + : { + type: 'launch', + description: 'Launch Drizzle Studio', + actions: [{ + label: promise ? 'Starting...' : 'Launch', + pending: isReady, + handle() { + promise = promise || launchDrizzleStudio(nuxt) + return promise + } + }] + } + }) }) }