Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ pnpm dev:prepare
# Develop with the playground
pnpm dev

# Playground only
pnpm dev:playground

# Build the playground
pnpm dev:build

Expand Down
19 changes: 18 additions & 1 deletion build.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ export default defineBuildConfig({
'drizzle-orm',
'drizzle-kit',
'./db/lib',
'db/lib'
'db/lib',
'sirv',
'./devtools/setup',
'devtools/setup'
],
entries: [
'src/module',
Expand Down Expand Up @@ -50,6 +53,20 @@ export default defineBuildConfig({
input: 'src/cache/runtime/',
outDir: 'dist/cache/runtime',
builder: 'mkdist'
},
// DevTools
{
input: 'src/devtools/',
outDir: 'dist/devtools',
builder: 'mkdist',
// Only ship the Nuxt DevTools integration code.
// Avoid accidentally bundling local/generated UI artifacts (e.g. `src/devtools/client/.nuxt`).
pattern: ['*.ts']
},
{
input: 'src/devtools-runtime/',
outDir: 'dist/devtools-runtime',
builder: 'mkdist'
}
]
})
38 changes: 33 additions & 5 deletions docs/content/docs/1.getting-started/1.index.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,43 @@ NuxtHub provides optional features to help you build full-stack applications:
NuxtHub also integrates with the [Nuxt DevTools](https://devtools.nuxt.com/) to provide a complete development experience.

::tabs
:::tabs-item{label="Database"}
:::div{label="Database"}
:img{alt="Nuxt DevTools Database" height="515" src="/images/landing/nuxt-devtools-database.png" width="915"}
:::
<!--
:::tabs-item{label="KV"}

:::div{label="KV"}
:img{alt="Nuxt DevTools KV" height="515" src="/images/landing/nuxt-devtools-kv.png" width="915"}
:::

:::tabs-item{label="Blob"}
:::div{label="Blob"}
:img{alt="Nuxt DevTools Blob" height="515" src="/images/landing/nuxt-devtools-blob.png" width="915"}
::: -->
:::
::

By default, the KV/Blob/Cache DevTools UI is loaded from `https://hub.nuxt.com` and embedded as an iframe. This avoids shipping a separate UI bundle with the module, but it requires an internet connection.

::note
Safari is currently not supported when the hosted UI (`https://...`) calls your local dev server (`http://localhost...`) due to mixed content restrictions. See [nuxt-hub/core#34 (comment)](https://github.com/nuxt-hub/core/issues/34#issuecomment-2027021483).
::

If you run your dev server on a non-default port and the UI points to the wrong origin, you can override the local origin passed to the hosted UI:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
hub: {
devtools: {
appOrigin: 'http://localhost:3010'
}
}
})
```

To disable storage tabs in Nuxt DevTools:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
hub: {
devtools: false
}
})
```
4 changes: 4 additions & 0 deletions docs/content/docs/3.blob/1.index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ description: Setup Blob Storage in your Nuxt application to store assets like im
navigation.title: Setup
---

::note
In development, you can inspect and manage blobs from Nuxt DevTools (NuxtHub Blob tab). The UI is hosted on `https://hub.nuxt.com` and embedded as an iframe.
::

## Getting Started

::steps{level="3"}
Expand Down
4 changes: 4 additions & 0 deletions docs/content/docs/4.kv/1.index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ description: Set up Key-Value Storage in your Nuxt application, including instal

NuxtHub Key Value Storage automatically configures [Nitro Storage](https://nitro.build/guide/storage), which is built on [unstorage](https://unstorage.unjs.io/).

::note
In development, you can inspect and manage KV from Nuxt DevTools (NuxtHub KV tab). The UI is hosted on `https://hub.nuxt.com` and embedded as an iframe.
::

## Getting Started

Enable the key-value storage in your NuxtHub project by adding the `kv` property to the `hub` object in your `nuxt.config.ts` file.
Expand Down
4 changes: 4 additions & 0 deletions docs/content/docs/5.cache/1.index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ description: Configure caching in your Nuxt application, including setup for pag

NuxtHub Cache automatically configures [Nitro's cache storage](https://nitro.build/guide/cache#customize-cache-storage). It allows you to cache API routes, server functions, and pages in your application.

::note
In development, you can inspect and manage cache entries from Nuxt DevTools (NuxtHub Cache tab). The UI is hosted on `https://hub.nuxt.com` and embedded as an iframe.
::

## Getting Started

Enable cache storage in your project by setting `cache: true` in the NuxtHub config.
Expand Down
Binary file added docs/public/images/landing/nuxt-devtools-blob.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ export default createConfigForNuxt({
'./docs'
]
}
}).append({
ignores: [
'**/.nuxt/**',
'**/.output/**',
'**/dist/**',
'**/.wrangler/**'
]
}).overrideRules({
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': ['error', {
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@
"scripts": {
"prepack": "nuxt-module-build build",
"dev:prepare": "nuxt-module-build build --stub && mkdist --src=src/blob/lib --dist=dist/blob/lib -d && mkdist --src=src/db/lib --dist=dist/db/lib -d && nuxt-module-build prepare && nuxi prepare playground",
"dev": "nuxi dev playground",
"dev": "pnpm dev:playground",
"dev:playground": "nuxi dev playground",
"dev:build": "nuxi build playground",
"docs": "PORT=4000 nuxi dev docs",
"docs:build": "nuxi generate docs",
Expand Down
42 changes: 42 additions & 0 deletions src/devtools-runtime/server/api/_hub/blob/[...pathname].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { eventHandler, createError, getRequestURL, readFormData } from 'h3'
import { blob } from 'hub:blob'
import { handleCors } from '../../../utils/cors'

function safeDecodeURIComponent(input: string) {
try {
return decodeURIComponent(input)
} catch {
return input
}
}

export default eventHandler(async (event) => {
if (handleCors(event)) return
const method = event.method
const baseRoute = '/api/_hub/blob/'
const urlPath = getRequestURL(event).pathname
const raw = urlPath.startsWith(baseRoute) ? urlPath.slice(baseRoute.length) : ''
const pathname = safeDecodeURIComponent(raw.replace(/^\/+/, ''))
if (!pathname) throw createError({ statusCode: 400, message: 'Pathname is required' })

if (method === 'GET') {
return blob.serve(event, pathname)
}

if (method === 'PUT') {
const form = await readFormData(event)
const file = form.get('file')
if (!(file instanceof File)) {
throw createError({ statusCode: 400, message: 'Missing file' })
}
await blob.put(pathname, file)
return 'OK'
}

if (method === 'DELETE') {
await blob.del(pathname)
return 'OK'
}

throw createError({ statusCode: 405, message: 'Method not allowed' })
})
20 changes: 20 additions & 0 deletions src/devtools-runtime/server/api/_hub/blob/head.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { eventHandler, getRequestURL } from 'h3'
import { blob } from 'hub:blob'
import { handleCors } from '../../../utils/cors'

function safeDecodeURIComponent(input: string) {
try {
return decodeURIComponent(input)
} catch {
return input
}
}

export default eventHandler(async (event) => {
if (handleCors(event)) return
const baseRoute = '/api/_hub/blob/head/'
const urlPath = getRequestURL(event).pathname
const raw = urlPath.startsWith(baseRoute) ? urlPath.slice(baseRoute.length) : ''
const pathname = safeDecodeURIComponent(raw.replace(/^\/+/, ''))
return blob.head(pathname)
})
14 changes: 14 additions & 0 deletions src/devtools-runtime/server/api/_hub/blob/index.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { eventHandler, getQuery } from 'h3'
import { blob } from 'hub:blob'
import { handleCors } from '../../../utils/cors'

export default eventHandler(async (event) => {
if (handleCors(event)) return
const { prefix = '', limit, cursor, folded } = getQuery(event)
return blob.list({
prefix: prefix as string,
limit: limit ? Number(limit) : undefined,
cursor: cursor as string,
folded: folded === 'true'
})
})
15 changes: 15 additions & 0 deletions src/devtools-runtime/server/api/_hub/blob/index.post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { eventHandler, getQuery } from 'h3'
import { destr } from 'destr'
import { blob } from 'hub:blob'
import { handleCors } from '../../../utils/cors'

export default eventHandler(async (event) => {
if (handleCors(event)) return
const query = getQuery(event)
return blob.handleUpload(event, {
formKey: (query.formKey as string) || 'files',
multiple: query.multiple !== 'false',
put: destr(query.put),
ensure: destr(query.ensure)
})
})
42 changes: 42 additions & 0 deletions src/devtools-runtime/server/api/_hub/cache/[...key].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { eventHandler, createError, sendNoContent, getRequestURL } from 'h3'
import { useStorage } from 'nitropack/runtime'
import { handleCors } from '../../../utils/cors'

function safeDecodeURIComponent(input: string) {
try {
return decodeURIComponent(input)
} catch {
return input
}
}

export default eventHandler(async (event) => {
if (handleCors(event)) return

const method = event.method
const baseRoute = '/api/_hub/cache/'
const urlPath = getRequestURL(event).pathname
const raw = urlPath.startsWith(baseRoute) ? urlPath.slice(baseRoute.length) : ''
const key = safeDecodeURIComponent(raw.replace(/^\/+/, ''))
if (!key) throw createError({ statusCode: 400, message: 'Key is required' })

const cache = useStorage('cache')

if (method === 'HEAD') {
const value = await cache.getItem(key)
if (value === null || value === undefined) throw createError({ statusCode: 404, message: 'Key not found' })
return sendNoContent(event)
}

if (method === 'GET') {
const value = await cache.getItem(key)
return value
}

if (method === 'DELETE') {
await cache.removeItem(key)
return 'OK'
}

throw createError({ statusCode: 405, message: 'Method not allowed' })
})
15 changes: 15 additions & 0 deletions src/devtools-runtime/server/api/_hub/cache/batch-delete.post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { eventHandler, readBody, createError } from 'h3'
import { useStorage } from 'nitropack/runtime'
import { handleCors } from '../../../utils/cors'

export default eventHandler(async (event) => {
if (handleCors(event)) return
const body = await readBody(event)
const keys = body?.keys
if (!Array.isArray(keys) || keys.some(k => typeof k !== 'string')) {
throw createError({ statusCode: 400, message: 'Expected body: { keys: string[] }' })
}
const cache = useStorage('cache')
await Promise.all(keys.map(k => cache.removeItem(k).catch(() => undefined)))
return { deleted: keys.length }
})
30 changes: 30 additions & 0 deletions src/devtools-runtime/server/api/_hub/cache/clear.delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { eventHandler, getRequestURL } from 'h3'
import { useStorage } from 'nitropack/runtime'
import { handleCors } from '../../../utils/cors'

function safeDecodeURIComponent(input: string) {
try {
return decodeURIComponent(input)
} catch {
return input
}
}

export default eventHandler(async (event) => {
if (handleCors(event)) return
const cache = useStorage('cache')

const baseRoute = '/api/_hub/cache/clear/'
const urlPath = getRequestURL(event).pathname
const raw = urlPath.startsWith(baseRoute) ? urlPath.slice(baseRoute.length) : ''
const base = safeDecodeURIComponent(raw.replace(/^\/+/, ''))

if (!base) {
await cache.clear()
return { cleared: true }
}

const keys = await cache.getKeys(base)
await Promise.all(keys.map(k => cache.removeItem(k).catch(() => undefined)))
return { cleared: true, deleted: keys.length }
})
18 changes: 18 additions & 0 deletions src/devtools-runtime/server/api/_hub/cache/index.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { eventHandler, getQuery } from 'h3'
import { useStorage } from 'nitropack/runtime'
import { handleCors } from '../../../utils/cors'

export default eventHandler(async (event) => {
if (handleCors(event)) return
const { prefix = '' } = getQuery(event)
const cache = useStorage('cache')
const keys = await cache.getKeys(prefix as string)
return Promise.all(keys.map(async (key) => {
try {
const value = await cache.getItem(key)
return { key, value }
} catch {
return { key, value: null }
}
}))
})
Loading
Loading