From fa4926e360ab5d7f7bc61e388b715cc8cb32ff43 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Thu, 18 Sep 2025 13:06:58 +0200 Subject: [PATCH 1/7] feat: use Vite and Webpack server for content hot reload --- package.json | 3 +- pnpm-lock.yaml | 9 +- src/module.ts | 16 ++- src/runtime/internal/websocket.ts | 101 -------------- src/runtime/plugins/websocket.dev.ts | 29 ++++- src/utils/dev.ts | 188 +++++++++++---------------- 6 files changed, 114 insertions(+), 232 deletions(-) delete mode 100644 src/runtime/internal/websocket.ts diff --git a/package.json b/package.json index d412e908b..4b7167199 100644 --- a/package.json +++ b/package.json @@ -72,10 +72,10 @@ "defu": "^6.1.4", "destr": "^2.0.5", "git-url-parse": "^16.1.0", + "hookable": "^5.5.3", "jiti": "^2.5.1", "json-schema-to-typescript": "^15.0.4", "knitwork": "^1.2.0", - "listhen": "^1.9.0", "mdast-util-to-hast": "^13.2.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.2", @@ -103,7 +103,6 @@ "unified": "^11.0.5", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", - "ws": "^8.18.3", "zod": "^3.25.76", "zod-to-json-schema": "^3.24.6" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c1f6ae94..65c37451c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ importers: git-url-parse: specifier: ^16.1.0 version: 16.1.0 + hookable: + specifier: ^5.5.3 + version: 5.5.3 jiti: specifier: ^2.5.1 version: 2.5.1 @@ -68,9 +71,6 @@ importers: knitwork: specifier: ^1.2.0 version: 1.2.0 - listhen: - specifier: ^1.9.0 - version: 1.9.0 mdast-util-to-hast: specifier: ^13.2.0 version: 13.2.0 @@ -155,9 +155,6 @@ importers: unist-util-visit: specifier: ^5.0.0 version: 5.0.0 - ws: - specifier: ^8.18.3 - version: 8.18.3 zod: specifier: ^3.25.76 version: 3.25.76 diff --git a/src/module.ts b/src/module.ts index d7c1ed73f..cbb3f26d2 100644 --- a/src/module.ts +++ b/src/module.ts @@ -11,6 +11,8 @@ import { updateTemplates, addComponent, installModule, + addVitePlugin, + addWebpackPlugin, } from '@nuxt/kit' import type { Nuxt } from '@nuxt/schema' import type { ModuleOptions as MDCModuleOptions } from '@nuxtjs/mdc' @@ -24,7 +26,7 @@ import { generateCollectionInsert, generateCollectionTableDefinition } from './u import { componentsManifestTemplate, contentTypesTemplate, fullDatabaseRawDumpTemplate, manifestTemplate, moduleTemplates } from './utils/templates' import type { ResolvedCollection } from './types/collection' import type { ModuleOptions } from './types/module' -import { getContentChecksum, logger, watchContents, chunks, watchComponents, startSocketServer } from './utils/dev' +import { getContentChecksum, logger, watchContents, chunks, watchComponents, NuxtContentHMRUnplugin } from './utils/dev' import { loadContentConfig } from './utils/config' import { createParser } from './utils/content' import { installMDCModule } from './utils/mdc' @@ -196,10 +198,16 @@ export default defineNuxtModule({ // Handle HMR changes if (nuxt.options.dev) { + + // Install unified HMR plugin for Vite/Webpack + addVitePlugin(NuxtContentHMRUnplugin.vite()) + if (typeof addWebpackPlugin === 'function') { + addWebpackPlugin(NuxtContentHMRUnplugin.webpack()) + } + addPlugin({ src: resolver.resolve('./runtime/plugins/websocket.dev'), mode: 'client' }) - await watchComponents(nuxt) - const socket = await startSocketServer(nuxt, options, manifest) - await watchContents(nuxt, options, manifest, socket) + watchContents(nuxt, options, manifest) + watchComponents(nuxt) } }) diff --git a/src/runtime/internal/websocket.ts b/src/runtime/internal/websocket.ts deleted file mode 100644 index 2d00f032c..000000000 --- a/src/runtime/internal/websocket.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { loadDatabaseAdapter } from './database.client' -import { useRuntimeConfig, refreshNuxtData } from '#imports' -import { joinURL } from 'ufo' - -const logger = { - log: (...args: unknown[]) => console.log('[Nuxt Content : Hot Content Reload]', ...args), - warn: (...args: unknown[]) => console.warn('[Nuxt Content : Hot Content Reload]', ...args), -} - -let ws: WebSocket | undefined - -export function useContentWebSocket() { - if (!window.WebSocket) { - logger.warn('Could not enable hot reload, your browser does not support WebSocket.') - return - } - - const onMessage = async (message: { data: string }) => { - try { - const data = JSON.parse(message.data) - - if (!data || !data.queries || !data.collection) { - return - } - - const db = await loadDatabaseAdapter(data.collection) - - await data.queries.reduce(async (prev: Promise, sql: string) => { - await prev - await db.exec(sql).catch((err: unknown) => console.log(err)) - }, Promise.resolve()) - - refreshNuxtData() - } - catch { - // Do nothing - } - } - - const onOpen = () => logger.log('WS connected!') - - const onError = (e: Event) => { - switch ((e as unknown as { code: string }).code) { - case 'ECONNREFUSED': - connect(true) - break - default: - logger.warn('WS Error:', e) - break - } - } - - const onClose = (e: { code?: number }) => { - // https://tools.ietf.org/html/rfc6455#section-11.7 - if (e.code === 1000 || e.code === 1005) { - // Normal close - logger.log('WS closed!') - } - else { - // Unkown error - connect(true) - } - } - - const connect = (retry = false) => { - if (retry) { - logger.log('WS reconnecting..') - setTimeout(connect, 1000) - return - } - - if (ws) { - try { - ws.close() - } - catch { - // Do nothing - } - ws = undefined - } - - // WebSocket Base URL - const wsURL = new URL(joinURL((useRuntimeConfig().public.content as { wsUrl: string }).wsUrl, 'ws')) - wsURL.protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' - - logger.log(`WS connect to ${wsURL}`) - - ws = new WebSocket(wsURL) - ws.onopen = onOpen - ws.onmessage = onMessage - ws.onerror = onError - ws.onclose = onClose - } - - // automatically connect on use - connect() - - return { - connect, - } -} diff --git a/src/runtime/plugins/websocket.dev.ts b/src/runtime/plugins/websocket.dev.ts index 50b7e0044..75cc99d2b 100644 --- a/src/runtime/plugins/websocket.dev.ts +++ b/src/runtime/plugins/websocket.dev.ts @@ -1,11 +1,28 @@ import { defineNuxtPlugin } from 'nuxt/app' -import { useRuntimeConfig } from '#imports' +import { refreshNuxtData } from '#imports' export default defineNuxtPlugin(() => { - const publicConfig = useRuntimeConfig().public.content as { wsUrl: string } + if (!import.meta.hot || !import.meta.client) return - if (import.meta.client && publicConfig.wsUrl) { - // Connect to websocket - import('../internal/websocket').then(({ useContentWebSocket }) => useContentWebSocket()) - } + import('../internal/database.client').then(({ loadDatabaseAdapter }) => { + + ;(import.meta.hot as any).on('nuxt-content:update', async (data: { collection: string, key: string, queries: string[] }) => { + if (!data || !data.collection || !Array.isArray(data.queries)) return + try { + const db = await loadDatabaseAdapter(data.collection) + for (const sql of data.queries) { + try { + await db.exec(sql) + } + catch (err) { + console.log(err) + } + } + refreshNuxtData() + } + catch { + // ignore + } + }) + }) }) diff --git a/src/utils/dev.ts b/src/utils/dev.ts index cd0b867f1..cc06b53ef 100644 --- a/src/utils/dev.ts +++ b/src/utils/dev.ts @@ -1,16 +1,13 @@ +import { createUnplugin } from 'unplugin' +import type { ViteDevServer } from 'vite' import crypto from 'node:crypto' import { readFile } from 'node:fs/promises' -import type { IncomingMessage } from 'node:http' import { join, resolve } from 'pathe' import type { Nuxt } from '@nuxt/schema' import { addVitePlugin, isIgnored, updateTemplates, useLogger } from '@nuxt/kit' import type { ConsolaInstance } from 'consola' import chokidar from 'chokidar' import micromatch from 'micromatch' -import type { WebSocket } from 'ws' -import { WebSocketServer } from 'ws' -import { listen } from 'listhen' -import type { Listener } from 'listhen' import { withTrailingSlash } from 'ufo' import type { ModuleOptions, ResolvedCollection } from '../types' import type { Manifest } from '../types/manifest' @@ -19,85 +16,50 @@ import { generateCollectionInsert } from './collection' import { createParser } from './content' import { moduleTemplates } from './templates' import { getExcludedSourcePaths, parseSourceBase } from './source' +import { createHooks } from 'hookable' export const logger: ConsolaInstance = useLogger('@nuxt/content') -export async function startSocketServer(nuxt: Nuxt, options: ModuleOptions, manifest: Manifest) { - const db = await getLocalDatabase(options._localDatabase!, { nativeSqlite: options.experimental?.nativeSqlite }) +export const contentHooks = createHooks<{ + 'hmr:content:update': (data: { key: string, collection: string, queries: string[] }) => void +}>() - let websocket: ReturnType - let listener: Listener - const websocketOptions = options.watch || {} - if (websocketOptions.enabled) { - nuxt.hook('nitro:init', async (nitro) => { - websocket = createWebSocket() - - // Listen dev server - listener = await listen(() => 'Nuxt Content', websocketOptions) - - // Register ws url - const publicConfig = nitro.options.runtimeConfig.public.content as Record - publicConfig.wsUrl = (websocketOptions.publicURL || listener.url).replace('http', 'ws') - - listener.server.on('upgrade', websocket.serve) - }) - - nuxt.hook('close', async () => { - // Close WebSocket server - if (websocket) { - await websocket.close() - } - // Close listener server - if (listener) { - await listener.close() - } - }) - } - - async function broadcast(collection: ResolvedCollection, key: string, insertQuery?: string[]) { - const removeQuery = `DELETE FROM ${collection.tableName} WHERE id = '${key.replace(/'/g, '\'\'')}';` - await db.exec(removeQuery) - if (insertQuery) { - await Promise.all(insertQuery.map(query => db.exec(query))) - } - - const collectionDump = manifest.dump[collection.name]! - const keyIndex = collectionDump.findIndex(item => item.includes(`'${key}'`)) - const indexToUpdate = keyIndex !== -1 ? keyIndex : collectionDump.length - const itemsToRemove = keyIndex === -1 ? 0 : 1 - - if (insertQuery) { - collectionDump.splice(indexToUpdate, itemsToRemove, ...insertQuery) - } - else { - collectionDump.splice(indexToUpdate, itemsToRemove) - } - - updateTemplates({ - filter: template => [ - moduleTemplates.manifest, - moduleTemplates.fullCompressedDump, - // moduleTemplates.raw, - ].includes(template.filename), - }) - - websocket?.broadcast({ - key, - collection: collection.name, - queries: insertQuery ? [removeQuery, ...insertQuery] : [removeQuery], - }) - } +export const NuxtContentHMRUnplugin = createUnplugin(() => { return { - broadcast, + name: 'nuxt-content-hmr-unplugin', + vite: { + name: 'nuxt-content-hmr-unplugin', + configureServer(server: ViteDevServer) { + contentHooks.hook('hmr:content:update', (data) => { + server.ws.send({ + type: 'custom', + event: 'nuxt-content:update', + data, + }) + }) + }, + }, + webpack(_compiler) { + contentHooks.hook('hmr:content:update', (_data) => { + // TODO: Implement webpack support + }) + return + }, } -} +}) -export async function watchContents(nuxt: Nuxt, options: ModuleOptions, manifest: Manifest, socket: Awaited>) { +export function watchContents(nuxt: Nuxt, options: ModuleOptions, manifest: Manifest) { const collectionParsers = {} as Record>> - const db = await getLocalDatabase(options._localDatabase!, { nativeSqlite: options.experimental?.nativeSqlite }) const collections = manifest.collections + let db: Awaited> + async function getDb() { + if (!db) { + db = await getLocalDatabase(options._localDatabase!, { nativeSqlite: options.experimental?.nativeSqlite }) + } + return db + } const sourceMap = collections.flatMap((c) => { if (c.source) { @@ -151,6 +113,7 @@ export async function watchContents(nuxt: Nuxt, options: ModuleOptions, manifest return false }) if (match) { + const db = await getDb() const { collection, source, cwd } = match // Remove the cwd prefix path = path.substring(cwd.length) @@ -183,7 +146,7 @@ export async function watchContents(nuxt: Nuxt, options: ModuleOptions, manifest } const { queries: insertQuery } = generateCollectionInsert(collection, JSON.parse(parsedContent)) - await socket.broadcast(collection, keyInCollection, insertQuery) + await broadcast(collection, keyInCollection, insertQuery) } } @@ -201,6 +164,7 @@ export async function watchContents(nuxt: Nuxt, options: ModuleOptions, manifest return false }) if (match) { + const db = await getDb() const { collection, source, cwd } = match // Remove the cwd prefix path = path.substring(cwd.length) @@ -212,15 +176,50 @@ export async function watchContents(nuxt: Nuxt, options: ModuleOptions, manifest await db.deleteDevelopmentCache(keyInCollection) - await socket.broadcast(collection, keyInCollection) + await broadcast(collection, keyInCollection) } } + async function broadcast(collection: ResolvedCollection, key: string, insertQuery?: string[]) { + const db = await getDb() + const removeQuery = `DELETE FROM ${collection.tableName} WHERE id = '${key.replace(/'/g, '\'\'')}';` + await db.exec(removeQuery) + if (insertQuery) { + await Promise.all(insertQuery.map(query => db.exec(query))) + } + + const collectionDump = manifest.dump[collection.name]! + const keyIndex = collectionDump.findIndex(item => item.includes(`'${key}'`)) + const indexToUpdate = keyIndex !== -1 ? keyIndex : collectionDump.length + const itemsToRemove = keyIndex === -1 ? 0 : 1 + + if (insertQuery) { + collectionDump.splice(indexToUpdate, itemsToRemove, ...insertQuery) + } + else { + collectionDump.splice(indexToUpdate, itemsToRemove) + } + + updateTemplates({ + filter: template => [ + moduleTemplates.manifest, + moduleTemplates.fullCompressedDump, + // moduleTemplates.raw, + ].includes(template.filename), + }) + + contentHooks.callHook('hmr:content:update', { + key, + collection: collection.name, + queries: insertQuery ? [removeQuery, ...insertQuery] : [removeQuery], + }) + } + nuxt.hook('close', async () => { if (watcher) { watcher.removeAllListeners() watcher.close() - db.close() + db?.close() } }) } @@ -280,43 +279,6 @@ export function watchConfig(nuxt: Nuxt) { }) } -/** - * WebSocket server useful for live content reload. - */ -export function createWebSocket() { - const wss = new WebSocketServer({ noServer: true }) - - const serve = (req: IncomingMessage, socket = req.socket, head: Buffer) => - wss.handleUpgrade(req, socket, head, (client: WebSocket) => { - wss.emit('connection', client, req) - }) - - const broadcast = (data: unknown) => { - const message = JSON.stringify(data) - - for (const client of wss.clients) { - try { - client.send(message) - } - catch (err) { - /* Ignore error (if client not ready to receive event) */ - console.log(err) - } - } - } - - return { - serve, - broadcast, - close: () => { - // disconnect all clients - wss.clients.forEach(client => client.close()) - // close the server - return new Promise(resolve => wss.close(resolve)) - }, - } -} - export function getContentChecksum(content: string) { return crypto .createHash('md5') From 42f65b30672f54c379d4e43aed98219291c938ff Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Thu, 18 Sep 2025 13:16:52 +0200 Subject: [PATCH 2/7] fix: remove unused options --- src/module.ts | 21 +++++++-------------- src/runtime/plugins/websocket.dev.ts | 4 ++-- src/types/module.ts | 3 +-- src/utils/dev.ts | 1 - 4 files changed, 10 insertions(+), 19 deletions(-) diff --git a/src/module.ts b/src/module.ts index cbb3f26d2..2975f052c 100644 --- a/src/module.ts +++ b/src/module.ts @@ -55,15 +55,7 @@ export default defineNuxtModule({ filename: '.data/content/contents.sqlite', }, preview: {}, - watch: { - enabled: true, - port: { - port: 4000, - portRange: [4000, 4040], - }, - hostname: 'localhost', - showURL: false, - }, + watch: { enabled: true }, renderer: { alias: {}, anchorLinks: { @@ -198,11 +190,12 @@ export default defineNuxtModule({ // Handle HMR changes if (nuxt.options.dev) { - - // Install unified HMR plugin for Vite/Webpack - addVitePlugin(NuxtContentHMRUnplugin.vite()) - if (typeof addWebpackPlugin === 'function') { - addWebpackPlugin(NuxtContentHMRUnplugin.webpack()) + if (options.watch?.enabled !== false) { + // Install unified HMR plugin for Vite/Webpack + addVitePlugin(NuxtContentHMRUnplugin.vite()) + if (typeof addWebpackPlugin === 'function') { + addWebpackPlugin(NuxtContentHMRUnplugin.webpack()) + } } addPlugin({ src: resolver.resolve('./runtime/plugins/websocket.dev'), mode: 'client' }) diff --git a/src/runtime/plugins/websocket.dev.ts b/src/runtime/plugins/websocket.dev.ts index 75cc99d2b..a9317c5c1 100644 --- a/src/runtime/plugins/websocket.dev.ts +++ b/src/runtime/plugins/websocket.dev.ts @@ -1,12 +1,12 @@ import { defineNuxtPlugin } from 'nuxt/app' import { refreshNuxtData } from '#imports' +type HotEvent = (event: 'nuxt-content:update', callback: (data: { collection: string, key: string, queries: string[] }) => void) => void export default defineNuxtPlugin(() => { if (!import.meta.hot || !import.meta.client) return import('../internal/database.client').then(({ loadDatabaseAdapter }) => { - - ;(import.meta.hot as any).on('nuxt-content:update', async (data: { collection: string, key: string, queries: string[] }) => { + ;(import.meta.hot as unknown as { on: HotEvent }).on('nuxt-content:update', async (data) => { if (!data || !data.collection || !Array.isArray(data.queries)) return try { const db = await loadDatabaseAdapter(data.collection) diff --git a/src/types/module.ts b/src/types/module.ts index f8d5eae8a..a5246f1dd 100644 --- a/src/types/module.ts +++ b/src/types/module.ts @@ -1,4 +1,3 @@ -import type { ListenOptions } from 'listhen' import type { LanguageRegistration, BuiltinLanguage as ShikiLang, BuiltinTheme as ShikiTheme, ThemeRegistrationAny, ThemeRegistrationRaw } from 'shiki' import type { GitInfo } from '../utils/git' import type { MarkdownPlugin } from './content' @@ -70,7 +69,7 @@ export interface ModuleOptions { * Development HMR * @default { enabled: true } */ - watch?: Partial & { enabled?: boolean } + watch?: { enabled?: boolean } renderer: { /** diff --git a/src/utils/dev.ts b/src/utils/dev.ts index cc06b53ef..759fee9ab 100644 --- a/src/utils/dev.ts +++ b/src/utils/dev.ts @@ -24,7 +24,6 @@ export const contentHooks = createHooks<{ 'hmr:content:update': (data: { key: string, collection: string, queries: string[] }) => void }>() - export const NuxtContentHMRUnplugin = createUnplugin(() => { return { name: 'nuxt-content-hmr-unplugin', From 13d295130a0091407326b1f74b3a6cd06cb13623 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Thu, 18 Sep 2025 13:20:24 +0200 Subject: [PATCH 3/7] chore: add missing unplugin dependency --- package.json | 1 + pnpm-lock.yaml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/package.json b/package.json index 4b7167199..55ee819f3 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ "unified": "^11.0.5", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", + "unplugin": "^2.3.10", "zod": "^3.25.76", "zod-to-json-schema": "^3.24.6" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 65c37451c..d83bc678b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -155,6 +155,9 @@ importers: unist-util-visit: specifier: ^5.0.0 version: 5.0.0 + unplugin: + specifier: ^2.3.10 + version: 2.3.10 zod: specifier: ^3.25.76 version: 3.25.76 From 3e0dd105252efc083e49807459d728a865f49abb Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Thu, 18 Sep 2025 14:44:37 +0200 Subject: [PATCH 4/7] simplify logics --- src/module.ts | 22 +++++++----------- src/utils/config.ts | 9 ++++---- src/utils/dev.ts | 54 +++++++++++++++++---------------------------- 3 files changed, 33 insertions(+), 52 deletions(-) diff --git a/src/module.ts b/src/module.ts index 2975f052c..c8d42aaf6 100644 --- a/src/module.ts +++ b/src/module.ts @@ -12,7 +12,6 @@ import { addComponent, installModule, addVitePlugin, - addWebpackPlugin, } from '@nuxt/kit' import type { Nuxt } from '@nuxt/schema' import type { ModuleOptions as MDCModuleOptions } from '@nuxtjs/mdc' @@ -26,7 +25,7 @@ import { generateCollectionInsert, generateCollectionTableDefinition } from './u import { componentsManifestTemplate, contentTypesTemplate, fullDatabaseRawDumpTemplate, manifestTemplate, moduleTemplates } from './utils/templates' import type { ResolvedCollection } from './types/collection' import type { ModuleOptions } from './types/module' -import { getContentChecksum, logger, watchContents, chunks, watchComponents, NuxtContentHMRUnplugin } from './utils/dev' +import { getContentChecksum, logger, chunks, NuxtContentHMRUnplugin } from './utils/dev' import { loadContentConfig } from './utils/config' import { createParser } from './utils/content' import { installMDCModule } from './utils/mdc' @@ -90,7 +89,7 @@ export default defineNuxtModule({ // Detect installed validators and them into content context await initiateValidatorsContext() - const { collections } = await loadContentConfig(nuxt) + const { collections } = await loadContentConfig(nuxt, options) manifest.collections = collections nuxt.options.vite.optimizeDeps ||= {} @@ -189,18 +188,13 @@ export default defineNuxtModule({ }) // Handle HMR changes - if (nuxt.options.dev) { - if (options.watch?.enabled !== false) { - // Install unified HMR plugin for Vite/Webpack - addVitePlugin(NuxtContentHMRUnplugin.vite()) - if (typeof addWebpackPlugin === 'function') { - addWebpackPlugin(NuxtContentHMRUnplugin.webpack()) - } - } - + if (nuxt.options.dev && options.watch?.enabled !== false) { addPlugin({ src: resolver.resolve('./runtime/plugins/websocket.dev'), mode: 'client' }) - watchContents(nuxt, options, manifest) - watchComponents(nuxt) + addVitePlugin(NuxtContentHMRUnplugin.vite({ + nuxt, + moduleOptions: options, + manifest, + })) } }) diff --git a/src/utils/config.ts b/src/utils/config.ts index 24098b94f..8cb279ac7 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -1,7 +1,7 @@ import { loadConfig, watchConfig, createDefineConfig } from 'c12' import { relative } from 'pathe' import type { Nuxt } from '@nuxt/schema' -import type { DefinedCollection } from '../types' +import type { DefinedCollection, ModuleOptions } from '../types' import { defineCollection, resolveCollections } from './collection' import { logger } from './dev' @@ -20,8 +20,9 @@ const defaultConfig: NuxtContentConfig = { export const defineContentConfig = createDefineConfig() -export async function loadContentConfig(nuxt: Nuxt) { - const loader: typeof watchConfig = nuxt.options.dev +export async function loadContentConfig(nuxt: Nuxt, options?: ModuleOptions) { + const watch = nuxt.options.dev && options?.watch?.enabled !== false + const loader: typeof watchConfig = watch ? opts => watchConfig({ ...opts, onWatch: (e) => { @@ -44,7 +45,7 @@ export async function loadContentConfig(nuxt: Nuxt) { // eslint-disable-next-line @typescript-eslint/no-explicit-any delete (globalThis as any).defineContentConfig - if (nuxt.options.dev) { + if (watch) { nuxt.hook('close', () => Promise.all(contentConfigs.map(c => c.unwatch())).then(() => {})) } diff --git a/src/utils/dev.ts b/src/utils/dev.ts index 759fee9ab..4623fc080 100644 --- a/src/utils/dev.ts +++ b/src/utils/dev.ts @@ -4,7 +4,7 @@ import crypto from 'node:crypto' import { readFile } from 'node:fs/promises' import { join, resolve } from 'pathe' import type { Nuxt } from '@nuxt/schema' -import { addVitePlugin, isIgnored, updateTemplates, useLogger } from '@nuxt/kit' +import { isIgnored, updateTemplates, useLogger } from '@nuxt/kit' import type { ConsolaInstance } from 'consola' import chokidar from 'chokidar' import micromatch from 'micromatch' @@ -24,12 +24,29 @@ export const contentHooks = createHooks<{ 'hmr:content:update': (data: { key: string, collection: string, queries: string[] }) => void }>() -export const NuxtContentHMRUnplugin = createUnplugin(() => { +interface HMRPluginOptions { + nuxt: Nuxt + moduleOptions: ModuleOptions + manifest: Manifest +} + +export const NuxtContentHMRUnplugin = createUnplugin((opts: HMRPluginOptions) => { + const { nuxt, moduleOptions, manifest } = opts + const componentsTemplatePath = join(nuxt.options.buildDir, 'content/components.ts') + + watchContents(nuxt, moduleOptions, manifest) + watchComponents(nuxt) + return { name: 'nuxt-content-hmr-unplugin', vite: { - name: 'nuxt-content-hmr-unplugin', configureServer(server: ViteDevServer) { + server.watcher.on('change', (file) => { + if (file === componentsTemplatePath) { + return server.ws.send({ type: 'full-reload' }) + } + }) + contentHooks.hook('hmr:content:update', (data) => { server.ws.send({ type: 'custom', @@ -39,12 +56,6 @@ export const NuxtContentHMRUnplugin = createUnplugin(() => { }) }, }, - webpack(_compiler) { - contentHooks.hook('hmr:content:update', (_data) => { - // TODO: Implement webpack support - }) - return - }, } }) @@ -251,31 +262,6 @@ export function watchComponents(nuxt: Nuxt) { }) } }) - // Reload page when content/components.ts is changed - addVitePlugin({ - name: 'reload', - configureServer(server) { - server.watcher.on('change', (file) => { - if (file === componentsTemplatePath) { - server.ws.send({ - type: 'full-reload', - }) - } - }) - }, - }) -} - -export function watchConfig(nuxt: Nuxt) { - nuxt.hook('nitro:init', async (nitro) => { - nitro.storage.watch(async (_event, key) => { - if ('root:content.config.ts' === key) { - logger.info(`\`${key.split(':').pop()}\` updated, restarting the Nuxt server...`) - - nuxt.hooks.callHook('restart', { hard: true }) - } - }) - }) } export function getContentChecksum(content: string) { From 82793dfad19a67361e9da5a2222eb6272ea6aac4 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Thu, 18 Sep 2025 15:15:29 +0200 Subject: [PATCH 5/7] up --- src/module.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/module.ts b/src/module.ts index c8d42aaf6..c0a563edd 100644 --- a/src/module.ts +++ b/src/module.ts @@ -92,9 +92,9 @@ export default defineNuxtModule({ const { collections } = await loadContentConfig(nuxt, options) manifest.collections = collections - nuxt.options.vite.optimizeDeps ||= {} - nuxt.options.vite.optimizeDeps.exclude ||= [] - nuxt.options.vite.optimizeDeps.exclude.push('@sqlite.org/sqlite-wasm') + nuxt.options.vite.optimizeDeps = defu(nuxt.options.vite.optimizeDeps, { + exclude: ['@sqlite.org/sqlite-wasm'] + }) // Ignore content directory files in building nuxt.options.ignore = [...(nuxt.options.ignore || []), 'content/**'] @@ -115,16 +115,18 @@ export default defineNuxtModule({ addComponent({ name: 'ContentRenderer', filePath: resolver.resolve('./runtime/components/ContentRenderer.vue') }) // Add Templates & aliases - nuxt.options.nitro.alias = nuxt.options.nitro.alias || {} addTemplate(fullDatabaseRawDumpTemplate(manifest)) - nuxt.options.alias['#content/components'] = addTemplate(componentsManifestTemplate(manifest)).dst - nuxt.options.alias['#content/manifest'] = addTemplate(manifestTemplate(manifest)).dst + nuxt.options.nitro.alias = defu(nuxt.options.nitro.alias, { + '#content/components': addTemplate(componentsManifestTemplate(manifest)).dst, + '#content/manifest': addTemplate(manifestTemplate(manifest)).dst, + }) // Add content types to Nuxt and Nitro const typesTemplateDst = addTypeTemplate(contentTypesTemplate(manifest.collections)).dst - nuxt.options.nitro.typescript ||= {} - nuxt.options.nitro.typescript.tsConfig = defu(nuxt.options.nitro.typescript.tsConfig, { - include: [typesTemplateDst], + nuxt.options.nitro.typescript = defu(nuxt.options.nitro.typescript, { + tsConfig: { + include: [typesTemplateDst], + } }) // Register user components From 5f97f685f6acedcc8963477100e6122559008239 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Thu, 18 Sep 2025 15:55:20 +0200 Subject: [PATCH 6/7] lint: fix --- src/module.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/module.ts b/src/module.ts index c0a563edd..e2cb9b7ea 100644 --- a/src/module.ts +++ b/src/module.ts @@ -93,7 +93,7 @@ export default defineNuxtModule({ manifest.collections = collections nuxt.options.vite.optimizeDeps = defu(nuxt.options.vite.optimizeDeps, { - exclude: ['@sqlite.org/sqlite-wasm'] + exclude: ['@sqlite.org/sqlite-wasm'], }) // Ignore content directory files in building @@ -126,7 +126,7 @@ export default defineNuxtModule({ nuxt.options.nitro.typescript = defu(nuxt.options.nitro.typescript, { tsConfig: { include: [typesTemplateDst], - } + }, }) // Register user components From 6098fbb89b826a4c344be05f7d9c25d25aea19c5 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Thu, 18 Sep 2025 16:43:47 +0200 Subject: [PATCH 7/7] fix: content aliases --- src/module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/module.ts b/src/module.ts index e2cb9b7ea..d3028c408 100644 --- a/src/module.ts +++ b/src/module.ts @@ -116,7 +116,7 @@ export default defineNuxtModule({ // Add Templates & aliases addTemplate(fullDatabaseRawDumpTemplate(manifest)) - nuxt.options.nitro.alias = defu(nuxt.options.nitro.alias, { + nuxt.options.alias = defu(nuxt.options.alias, { '#content/components': addTemplate(componentsManifestTemplate(manifest)).dst, '#content/manifest': addTemplate(manifestTemplate(manifest)).dst, })