diff --git a/.nuxtrc b/.nuxtrc new file mode 100644 index 0000000..820d356 --- /dev/null +++ b/.nuxtrc @@ -0,0 +1 @@ +typescript.includeWorkspace=true diff --git a/client/components/DatabaseDetail.vue b/client/components/DatabaseDetail.vue index e95cf43..489c6c5 100644 --- a/client/components/DatabaseDetail.vue +++ b/client/components/DatabaseDetail.vue @@ -81,8 +81,9 @@ function editDocument(document: any) { async function saveDocument(document: any, create = true) { const method = create ? rpc.createDocument : rpc.updateDocument const newDocument = await method(props.collection, document) + // TODO: show toast if (newDocument?.error) - return alert(newDocument.error.message) + return if (create) { if (!documents.value.length) { @@ -105,8 +106,9 @@ function discardEditing() { async function deleteDocument(document: any) { const newDocument = await rpc.deleteDocument(props.collection, document._id) + // TODO: show toast if (newDocument.deletedCount === 0) - return alert('Failed to delete document') + return documents.value = documents.value.filter((doc: any) => doc._id !== document._id) } diff --git a/client/composables/rpc.ts b/client/composables/rpc.ts index 75ef8b5..b13209a 100644 --- a/client/composables/rpc.ts +++ b/client/composables/rpc.ts @@ -1,14 +1,14 @@ import { createBirpc } from 'birpc' import { parse, stringify } from 'flatted' +import { createHotContext } from 'vite-hot-client' import type { ClientFunctions, ServerFunctions } from '../../src/types' -import { PATH_ENTRY } from '../../src/constants' - -const RECONNECT_INTERVAL = 2000 +import { WS_EVENT_NAME } from '../../src/constants' export const wsConnecting = ref(true) export const wsError = ref() +export const wsConnectingDebounced = useDebounce(wsConnecting, 2000) -let connectPromise = connectWS() +const connectPromise = connectVite() let onMessage: Function = () => {} export const clientFunctions = { @@ -19,9 +19,11 @@ export const extendedRpcMap = new Map() export const rpc = createBirpc(clientFunctions, { post: async (d) => { - (await connectPromise).send(d) + (await connectPromise).send(WS_EVENT_NAME, d) + }, + on: (fn) => { + onMessage = fn }, - on: (fn) => { onMessage = fn }, serialize: stringify, deserialize: parse, resolver(name, fn) { @@ -35,35 +37,22 @@ export const rpc = createBirpc(clientFunctions, { onError(error, name) { console.error(`[nuxt-devtools] RPC error on executing "${name}":`, error) }, + timeout: 120_000, }) -async function connectWS() { - const wsUrl = new URL(`ws://host${PATH_ENTRY}`) - wsUrl.protocol = location.protocol === 'https:' ? 'wss:' : 'ws:' - wsUrl.host = 'localhost:3000' +async function connectVite() { + const hot = await createHotContext() - const ws = new WebSocket(wsUrl.toString()) - ws.addEventListener('message', e => onMessage(String(e.data))) - ws.addEventListener('error', (e) => { - console.error(e) - wsError.value = e + if (!hot) + throw new Error('Unable to connect to devtools') + + hot.on(WS_EVENT_NAME, (data) => { + onMessage(data) }) - ws.addEventListener('close', () => { - // eslint-disable-next-line no-console - console.log('[nuxt-devtools] WebSocket closed, reconnecting...') - wsConnecting.value = true - setTimeout(async () => { - connectPromise = connectWS() - }, RECONNECT_INTERVAL) - }) - wsConnecting.value = true - if (ws.readyState !== WebSocket.OPEN) - await new Promise(resolve => ws.addEventListener('open', resolve)) - // eslint-disable-next-line no-console - console.log('[nuxt-devtools] WebSocket connected.') - wsConnecting.value = false - wsError.value = null + // TODO: + // hot.on('vite:connect', (data) => {}) + // hot.on('vite:disconnect', (data) => {}) - return ws + return hot } diff --git a/package.json b/package.json index 070cee9..6df0cb8 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,6 @@ "require": "./module.cjs", "import": "./dist/module.mjs" }, - "./types": { - "types": "./dist/types.d.ts", - "import": "./dist/types.mjs" - }, "./*": "./*" }, "main": "./module.cjs", @@ -46,10 +42,11 @@ "flatted": "^3.2.7", "fs-extra": "^11.1.1", "mongoose": "^7.2.2", + "ofetch": "^1.1.0", "pathe": "^1.1.0", "pluralize": "^8.0.0", "sirv": "^2.0.3", - "tinyws": "^0.1.0", + "vite-hot-client": "^0.2.1", "ws": "^8.13.0" }, "devDependencies": { @@ -69,4 +66,4 @@ "splitpanes": "^3.1.5", "vitest": "^0.31.2" } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46a69d0..a14b272 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,8 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false dependencies: '@nuxt/devtools-kit': @@ -25,6 +29,9 @@ dependencies: mongoose: specifier: ^7.2.2 version: 7.2.2 + ofetch: + specifier: ^1.1.0 + version: 1.1.0 pathe: specifier: ^1.1.0 version: 1.1.0 @@ -34,9 +41,9 @@ dependencies: sirv: specifier: ^2.0.3 version: 2.0.3 - tinyws: - specifier: ^0.1.0 - version: 0.1.0(ws@8.13.0) + vite-hot-client: + specifier: ^0.2.1 + version: 0.2.1(vite@4.3.9) ws: specifier: ^8.13.0 version: 8.13.0 @@ -1150,7 +1157,7 @@ packages: mri: 1.2.0 nanoid: 4.0.2 node-fetch: 3.3.1 - ofetch: 1.0.1 + ofetch: 1.1.0 parse-git-config: 3.0.0 rc9: 2.1.0 std-env: 3.3.3 @@ -1180,7 +1187,7 @@ packages: defu: 6.1.2 execa: 7.1.1 get-port-please: 3.0.1 - ofetch: 1.0.1 + ofetch: 1.1.0 pathe: 1.1.0 ufo: 1.1.2 vitest: 0.31.2(sass@1.62.1) @@ -1820,7 +1827,7 @@ packages: dependencies: '@iconify/utils': 2.1.5 '@unocss/core': 0.52.5 - ofetch: 1.0.1 + ofetch: 1.1.0 transitivePeerDependencies: - supports-color dev: true @@ -1857,7 +1864,7 @@ packages: resolution: {integrity: sha512-oynjtFC05NQM1RXith7LM0MCQGqy4yF0tFRnpM8zmq3p6L7XPFitL5Lmx/walXOB7bw9QSL+gMjamdpEo+KjQg==} dependencies: '@unocss/core': 0.52.5 - ofetch: 1.0.1 + ofetch: 1.1.0 dev: true /@unocss/preset-wind@0.52.5: @@ -2900,7 +2907,7 @@ packages: execa: 7.1.1 mri: 1.2.0 node-fetch-native: 1.1.1 - ofetch: 1.0.1 + ofetch: 1.1.0 open: 9.1.0 pathe: 1.1.0 pkg-types: 1.0.3 @@ -4385,7 +4392,7 @@ packages: defu: 6.1.2 https-proxy-agent: 5.0.1 mri: 1.2.0 - node-fetch-native: 1.1.1 + node-fetch-native: 1.2.0 pathe: 1.1.0 tar: 6.1.15 transitivePeerDependencies: @@ -5827,8 +5834,8 @@ packages: mime: 3.0.0 mlly: 1.3.0 mri: 1.2.0 - node-fetch-native: 1.1.1 - ofetch: 1.0.1 + node-fetch-native: 1.2.0 + ofetch: 1.1.0 ohash: 1.1.2 openapi-typescript: 6.2.6 pathe: 1.1.0 @@ -5868,6 +5875,10 @@ packages: /node-fetch-native@1.1.1: resolution: {integrity: sha512-9VvspTSUp2Sxbl+9vbZTlFGq9lHwE8GDVVekxx6YsNd1YH59sb3Ba8v3Y3cD8PkLNcileGGcA21PFjVl0jzDaw==} + dev: true + + /node-fetch-native@1.2.0: + resolution: {integrity: sha512-5IAMBTl9p6PaAjYCnMv5FmqIF6GcZnawAVnzaCG0rX2aYZJ4CxEkZNtVPuTRug7fL7wyM5BQYTlAzcyMPi6oTQ==} /node-fetch@2.6.11: resolution: {integrity: sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==} @@ -6106,7 +6117,7 @@ packages: nitropack: 2.4.1 nuxi: 3.5.2 nypm: 0.2.0 - ofetch: 1.0.1 + ofetch: 1.1.0 ohash: 1.1.2 pathe: 1.1.0 perfect-debounce: 1.0.0 @@ -6200,11 +6211,11 @@ packages: es-abstract: 1.21.2 dev: true - /ofetch@1.0.1: - resolution: {integrity: sha512-icBz2JYfEpt+wZz1FRoGcrMigjNKjzvufE26m9+yUiacRQRHwnNlGRPiDnW4op7WX/MR6aniwS8xw8jyVelF2g==} + /ofetch@1.1.0: + resolution: {integrity: sha512-yjq2ZUUMto1ITpge2J5vNlUfteLzxfHn9aJC55WtVGD3okKwSfPoLaKpcHXmmKd2kZZUGo+jdkFuuj09Blyeig==} dependencies: destr: 1.2.2 - node-fetch-native: 1.1.1 + node-fetch-native: 1.2.0 ufo: 1.1.2 /ohash@1.1.2: @@ -7661,15 +7672,6 @@ packages: engines: {node: '>=14.0.0'} dev: true - /tinyws@0.1.0(ws@8.13.0): - resolution: {integrity: sha512-6WQ2FlFM7qm6lAXxeKnzsAEfmnBHz5W5EwonNs52V0++YfK1IoCCAWM429afcChFE9BFrDgOFnq7ligaWMsa/A==} - engines: {node: '>=12.4'} - peerDependencies: - ws: '>=8' - dependencies: - ws: 8.13.0 - dev: false - /titleize@3.0.0: resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==} engines: {node: '>=12'} @@ -7887,7 +7889,7 @@ packages: consola: 3.1.0 defu: 6.1.2 mime: 3.0.0 - node-fetch-native: 1.1.1 + node-fetch-native: 1.2.0 pathe: 1.1.0 /unhead@1.1.27: @@ -8064,8 +8066,8 @@ packages: listhen: 1.0.4 lru-cache: 9.1.1 mri: 1.2.0 - node-fetch-native: 1.1.1 - ofetch: 1.0.1 + node-fetch-native: 1.2.0 + ofetch: 1.1.0 ufo: 1.1.2 transitivePeerDependencies: - supports-color @@ -8152,6 +8154,14 @@ packages: builtins: 5.0.1 dev: true + /vite-hot-client@0.2.1(vite@4.3.9): + resolution: {integrity: sha512-UqsQdw5PODnSrTDT85nr09RlhV0gkm2Xat74U2l8JZ5R8M/wTCggWSyPjxbLk5fbbVnWfr0JwW+vVoosjQnYrA==} + peerDependencies: + vite: ^2.6.0 || ^3.0.0 || ^4.0.0 + dependencies: + vite: 4.3.9(@types/node@20.2.5)(sass@1.62.1) + dev: false + /vite-node@0.31.2(@types/node@20.2.5)(sass@1.62.1): resolution: {integrity: sha512-NvoO7+zSvxROC4JY8cyp/cO7DHAX3dwMOHQVDdNtCZ4Zq8wInnR/bJ/lfsXqE6wrUgtYCE5/84qHS+A7vllI3A==} engines: {node: '>=v14.18.0'} diff --git a/src/constants/index.ts b/src/constants/index.ts index 680c265..decbf5a 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,3 +1,3 @@ export const PATH = '/__nuxt_mongoose__' -export const PATH_ENTRY = `${PATH}/entry` export const PATH_CLIENT = `${PATH}/client` +export const WS_EVENT_NAME = 'nuxt:devtools:mongoose:rpc' diff --git a/src/module.ts b/src/module.ts index 54fd2a0..2b189b7 100644 --- a/src/module.ts +++ b/src/module.ts @@ -2,17 +2,19 @@ import { addImportsDir, addServerPlugin, addTemplate, + addVitePlugin, createResolver, defineNuxtModule, logger, } from '@nuxt/kit' import { pathExists } from 'fs-extra' -import { tinyws } from 'tinyws' import { join } from 'pathe' import { defu } from 'defu' import sirv from 'sirv' +import { $fetch } from 'ofetch' +import { version } from '../package.json' -import { PATH_CLIENT, PATH_ENTRY } from './constants' +import { PATH_CLIENT } from './constants' import type { ModuleOptions } from './types' import { setupRPC } from './server-rpc' @@ -34,6 +36,13 @@ export default defineNuxtModule({ const { resolve } = createResolver(import.meta.url) const runtimeConfig = nuxt.options.runtimeConfig + if (nuxt.options.dev) { + $fetch('https://registry.npmjs.org/nuxt-mongoose/latest').then((release) => { + if (release.version > version) + logger.info(`A new version of Nuxt Mongoose (v${release.version}) is available: https://github.com/arashsheyda/nuxt-mongoose/releases/latest`) + }).catch(() => {}) + } + addImportsDir(resolve('./runtime/composables')) if (!options.uri) { @@ -58,11 +67,11 @@ export default defineNuxtModule({ } const clientPath = distResolve('./client') - const { middleware: rpcMiddleware } = setupRPC(nuxt, options) + const { vitePlugin } = setupRPC(nuxt, options) + + addVitePlugin(vitePlugin) nuxt.hook('vite:serverCreated', async (server) => { - server.middlewares.use(PATH_ENTRY, tinyws() as any) - server.middlewares.use(PATH_ENTRY, rpcMiddleware as any) if (await pathExists(clientPath)) server.middlewares.use(PATH_CLIENT, sirv(clientPath, { dev: true, single: true })) }) diff --git a/src/runtime/server/services/mongoose.ts b/src/runtime/server/services/mongoose.ts index 30f4912..b6fd073 100644 --- a/src/runtime/server/services/mongoose.ts +++ b/src/runtime/server/services/mongoose.ts @@ -11,7 +11,7 @@ export async function defineMongooseConnection({ uri, options }: { uri?: string; try { await mongoose.connect(mongooseUri, { ...mongooseOptions }) - logger.info('Connected to mongoose database') + logger.info('Connected to MONGODB') } catch (err) { logger.error('Error connecting to database', err) diff --git a/src/server-rpc/index.ts b/src/server-rpc/index.ts index 8fa84b1..73e5ade 100644 --- a/src/server-rpc/index.ts +++ b/src/server-rpc/index.ts @@ -1,12 +1,12 @@ -import type { TinyWSRequest } from 'tinyws' -import type { NodeIncomingMessage, NodeServerResponse } from 'h3' import type { WebSocket } from 'ws' import { createBirpcGroup } from 'birpc' import type { ChannelOptions } from 'birpc' import { parse, stringify } from 'flatted' +import type { Plugin } from 'vite' import type { Nuxt } from 'nuxt/schema' import type { ClientFunctions, ModuleOptions, NuxtDevtoolsServerContext, ServerFunctions } from '../types' +import { WS_EVENT_NAME } from '../constants' import { setupDatabaseRPC } from './database' import { setupResourceRPC } from './resource' @@ -68,36 +68,50 @@ export function setupRPC(nuxt: Nuxt, options: ModuleOptions): any { } satisfies Partial) const wsClients = new Set() - const middleware = async (req: NodeIncomingMessage & TinyWSRequest, _res: NodeServerResponse, next: Function) => { - // Handle WebSocket - if (req.ws) { - const ws = await req.ws() - wsClients.add(ws) - const channel: ChannelOptions = { - post: d => ws.send(d), - on: fn => ws.on('message', fn), - serialize: stringify, - deserialize: parse, - } - rpc.updateChannels((c) => { - c.push(channel) - }) - ws.on('close', () => { - wsClients.delete(ws) + + const vitePlugin: Plugin = { + name: 'nuxt:devtools:rpc', + configureServer(server) { + server.ws.on('connection', (ws) => { + wsClients.add(ws) + const channel: ChannelOptions = { + post: d => ws.send(JSON.stringify({ + type: 'custom', + event: WS_EVENT_NAME, + data: d, + })), + on: (fn) => { + ws.on('message', (e) => { + try { + const data = JSON.parse(String(e)) || {} + if (data.type === 'custom' && data.event === WS_EVENT_NAME) { + // console.log(data.data) + fn(data.data) + } + } + catch {} + }) + }, + serialize: stringify, + deserialize: parse, + } rpc.updateChannels((c) => { - const index = c.indexOf(channel) - if (index >= 0) - c.splice(index, 1) + c.push(channel) + }) + ws.on('close', () => { + wsClients.delete(ws) + rpc.updateChannels((c) => { + const index = c.indexOf(channel) + if (index >= 0) + c.splice(index, 1) + }) }) }) - } - else { - next() - } + }, } return { - middleware, + vitePlugin, ...ctx, } }