Skip to content

Commit

Permalink
feat!: module option to enable features (#42)
Browse files Browse the repository at this point in the history
* feat(module): module options to toggle NuxtHub features

* feat: write `dist/hub.config.json`

* chore: update module options type

* fix: set features default value to false

* docs: add module options

* fix: respect cache feature

* fix(dev-tools): register panel if feature is enabled

* fix: send features on `build:before` hook

* chore: remove `c12` workaround, issue fixed in current version

* chore: simplify module setup function

* fix: wranger template

* Update docs/content/docs/1.getting-started/2.installation.md

* Apply suggestions from code review

* docs: up

---------

Co-authored-by: Sébastien Chopin <seb@nuxt.com>
  • Loading branch information
farnabaz and atinux authored Apr 9, 2024
1 parent e4f42f7 commit a479ec8
Show file tree
Hide file tree
Showing 27 changed files with 323 additions and 102 deletions.
20 changes: 20 additions & 0 deletions docs/content/docs/1.getting-started/2.installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,26 @@ export default defineNuxtConfig({
Default to `false` - Allows working with remote storage (database, kv, blob) from your deployed project. :br
[Read more about remote storage for usage](/docs/getting-started/remote-storage).
::

::field{name="analytics" type="boolean"}
Default to `false` - Enables analytics for your project (coming soon).
::

::field{name="blob" type="boolean"}
Default to `false` - Enables blob storage to store static assets, such as images, videos and more.
::

::field{name="cache" type="boolean"}
Default to `false` - Enables cache storage to cache your server route responses or functions using Nitro's `cachedEventHandler` and `cachedFunction`
::

::field{name="database" type="boolean"}
Default to `false` - Enables SQL database to store your application's data.
::

::field{name="kv" type="boolean"}
Default to `false` - Enables Key-Value to store JSON data accessible globally.
::
::

::tip{icon="i-ph-rocket-launch-duotone"}
Expand Down
12 changes: 12 additions & 0 deletions docs/content/docs/2.storage/1.database.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ description: How to create a database and store entries with NuxtHub.

NuxtHub Database is a layer on top of [Cloudflare D1](https://developers.cloudflare.com/d1), a serverless SQLite databases.

## Getting Started

Enable the database in your NuxtHub project by adding the `database` property to the `hub` object in your `nuxt.config.ts` file.

```ts [nuxt.config.ts]
defineNuxtConfig({
hub: {
database: true
}
})
```

## `hubDatabase()`

Server composable that returns a [D1 database client](https://developers.cloudflare.com/d1/build-databases/query-databases/).
Expand Down
12 changes: 12 additions & 0 deletions docs/content/docs/2.storage/2.kv.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ description: How to use key-value data storage with NuxtHub.

NuxtHub KV is a layer on top of [Cloudflare Workers KV](https://developers.cloudflare.com/kv), a global, low-latency, key-value data storage.

## 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.

```ts [nuxt.config.ts]
defineNuxtConfig({
hub: {
kv: true
}
})
```

## `hubKV()`

Server method that returns an [unstorage instance](https://unstorage.unjs.io/guide#interface) with `keys()`, `get()`, `set()` and `del()` aliases.
Expand Down
12 changes: 12 additions & 0 deletions docs/content/docs/2.storage/3.blob.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ description: How to store objects with NuxtHub.

NuxtHub Blob is a layer on top of [Cloudflare R2](https://developers.cloudflare.com/r2), allowing to store large amounts of unstructured data (images, videos, etc.).

## Getting Started

Enable the blob storage in your NuxtHub project by adding the `blob` property to the `hub` object in your `nuxt.config.ts` file.

```ts [nuxt.config.ts]
defineNuxtConfig({
hub: {
blob: true
}
})
```

## `hubBlob()`

Server composable that returns a set of methods to manipulate the blob storage.
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
"ofetch": "^1.3.4",
"pathe": "^1.1.2",
"pkg-types": "^1.0.3",
"rc9": "^2.1.1",
"ufo": "^1.5.3",
"uncrypto": "^0.1.3",
"unstorage": "^1.10.2",
Expand Down
3 changes: 0 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

142 changes: 72 additions & 70 deletions src/module.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,52 @@
import { defineNuxtModule, createResolver, logger, addServerScanDir, installModule, addServerImportsDir } from '@nuxt/kit'
import { addCustomTab } from '@nuxt/devtools-kit'
import { join } from 'pathe'
import { defu } from 'defu'
import { mkdir, writeFile, readFile } from 'node:fs/promises'
import { findWorkspaceDir } from 'pkg-types'
import { readUser } from 'rc9'
import { $fetch } from 'ofetch'
import { joinURL } from 'ufo'
import { parseArgs } from 'citty'
import { generateWrangler } from './utils'
import { addDevtoolsCustomTabs, generateWrangler } from './utils'
import { version } from '../package.json'
import { execSync } from 'node:child_process'
import { argv } from 'node:process'

const log = logger.withTag('nuxt:hub')

export interface ModuleOptions {
/**
* Set `true` to enable the analytics for the project.
*
* @default false
*/
analytics?: boolean
/**
* Set `true` to enable the Blob storage for the project.
*
* @default false
*/
blob?: boolean
/**
* Set `true` to enable caching for the project.
*
* @default false
* @see https://hub.nuxt.com/docs/storage/blob
*/
cache?: boolean
/**
* Set `true` to enable the database for the project.
*
* @default false
* @see https://hub.nuxt.com/docs/storage/database
*/
database?: boolean
/**
* Set `true` to enable the Key-Value storage for the project.
*
* @default false
* @see https://hub.nuxt.com/docs/storage/kv
*/
kv?: boolean
/**
* Set to `true`, 'preview' or 'production' to use the remote storage.
* Only set the value on a project you are deploying outside of NuxtHub or Cloudflare.
Expand Down Expand Up @@ -61,13 +92,6 @@ export default defineNuxtModule<ModuleOptions>({
async setup (options, nuxt) {
const rootDir = nuxt.options.rootDir
const { resolve } = createResolver(import.meta.url)
const resolveRuntimeModule = (path: string) => resolve('./runtime', path)

// Waiting for https://github.com/unjs/c12/pull/139
// Then adding the c12 dependency to the project to 1.8.1
options = defu(options, {
...readUser('.nuxtrc').hub,
})

let remoteArg = parseArgs(argv, { remote: { type: 'string' } }).remote as string
remoteArg = (remoteArg === '' ? 'true' : remoteArg)
Expand All @@ -82,6 +106,12 @@ export default defineNuxtModule<ModuleOptions>({
userToken: process.env.NUXT_HUB_USER_TOKEN || '',
// Remote storage
remote: remoteArg || process.env.NUXT_HUB_REMOTE,
// NuxtHub features
analytics: false,
blob: false,
cache: false,
database: false,
kv: false,
// Other options
version,
env: process.env.NUXT_HUB_ENV || 'production',
Expand All @@ -97,30 +127,32 @@ export default defineNuxtModule<ModuleOptions>({
log.info(`Using \`${hub.url}\` as NuxtHub Admin URL`)
}

// Add Server caching (Nitro)
nuxt.options.nitro = defu(nuxt.options.nitro, {
storage: {
cache: {
driver: 'cloudflare-kv-binding',
binding: 'CACHE',
base: 'cache'
}
},
devStorage: {
cache: {
driver: 'fs',
base: join(rootDir, '.data/cache')
if (hub.cache) {
// Add Server caching (Nitro)
nuxt.options.nitro = defu(nuxt.options.nitro, {
storage: {
cache: {
driver: 'cloudflare-kv-binding',
binding: 'CACHE',
base: 'cache'
}
},
devStorage: {
cache: {
driver: 'fs',
base: join(rootDir, '.data/cache')
}
}
}
})
})
}

// nuxt prepare, stop here
if (nuxt.options._prepare) {
return
}

// Register composables
addServerImportsDir(resolveRuntimeModule('./server/utils'))
addServerImportsDir(resolve('./runtime/server/utils'))

// Within CF Pages CI/CD to notice NuxtHub about the build and hub config
if (!nuxt.options.dev && process.env.CF_PAGES && process.env.NUXT_HUB_PROJECT_DEPLOY_TOKEN && process.env.NUXT_HUB_PROJECT_KEY && process.env.NUXT_HUB_ENV) {
Expand All @@ -132,11 +164,11 @@ export default defineNuxtModule<ModuleOptions>({
authorization: `Bearer ${process.env.NUXT_HUB_PROJECT_DEPLOY_TOKEN}`
},
body: {
analytics: true,
blob: true,
cache: true,
database: true,
kv: true
analytics: hub.analytics,
blob: hub.blob,
cache: hub.cache,
database: hub.database,
kv: hub.kv
},
}).catch(() => {})
})
Expand All @@ -155,11 +187,11 @@ export default defineNuxtModule<ModuleOptions>({
// Write `dist/hub.config.json` after public assets are built
nuxt.hook('nitro:build:public-assets', async (nitro) => {
const hubConfig = {
analytics: true,
blob: true,
cache: true,
database: true,
kv: true
analytics: hub.analytics,
blob: hub.blob,
cache: hub.cache,
database: hub.database,
kv: hub.kv
}
await writeFile(join(nitro.options.output.publicDir, 'hub.config.json'), JSON.stringify(hubConfig, null, 2), 'utf-8')
})
Expand Down Expand Up @@ -248,44 +280,14 @@ export default defineNuxtModule<ModuleOptions>({
logger.info(`Remote storage available: ${Object.keys(manifest.storage).filter(k => manifest.storage[k]).map(k => `\`${k}\``).join(', ')} `)
}

// Add Proxy routes only if not remote or in development (used for devtools)
if (nuxt.options.dev || !hub.remote) {
// Add Proxy routes only if not remote or in development (used for devtools)
addServerScanDir(resolve('./runtime/server'))
}

// Add custom tabs to Nuxt Devtools
if (nuxt.options.dev) {
nuxt.hook('listen', (_, { url }) => {
addCustomTab({
category: 'server',
name: 'hub-database',
title: 'Hub Database',
icon: 'i-ph-database',
view: {
type: 'iframe',
src: `https://admin.hub.nuxt.com/embed/database?url=${url}`,
},
})
addCustomTab({
category: 'server',
name: 'hub-kv',
title: 'Hub KV',
icon: 'i-ph-coin',
view: {
type: 'iframe',
src: `https://admin.hub.nuxt.com/embed/kv?url=${url}`,
},
})
addCustomTab({
category: 'server',
name: 'hub-blob',
title: 'Hub Blob',
icon: 'i-ph-shapes',
view: {
type: 'iframe',
src: `https://admin.hub.nuxt.com/embed/blob?url=${url}`,
},
})
})
addDevtoolsCustomTabs(nuxt, hub)
}

// Local development without remote connection
Expand Down Expand Up @@ -313,7 +315,7 @@ export default defineNuxtModule<ModuleOptions>({

// Generate the wrangler.toml file
const wranglerPath = join(hubDir, './wrangler.toml')
await writeFile(wranglerPath, generateWrangler(), 'utf-8')
await writeFile(wranglerPath, generateWrangler(hub), 'utf-8')
// @ts-ignore
nuxt.options.nitro.cloudflareDev = {
persistDir: hubDir,
Expand Down
5 changes: 5 additions & 0 deletions src/runtime/server/api/_hub/analytics/index.put.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ import type { AnalyticsEngineDataPoint } from '@cloudflare/workers-types/experim
import { eventHandler, readValidatedBody } from 'h3'
import { z } from 'zod'
import { hubAnalytics } from '../../../utils/analytics'
import { requireNuxtHubAuthorization } from '../../../utils/auth'
import { requireNuxtHubFeature } from '../../../utils/features'

export default eventHandler(async (event) => {
await requireNuxtHubAuthorization(event)
requireNuxtHubFeature('analytics')

const { data } = await readValidatedBody(event, z.object({
data: z.custom<AnalyticsEngineDataPoint>()
}).parse)
Expand Down
5 changes: 5 additions & 0 deletions src/runtime/server/api/_hub/blob/[...pathname].delete.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { eventHandler, getValidatedRouterParams, sendNoContent } from 'h3'
import { z } from 'zod'
import { hubBlob } from '../../../utils/blob'
import { requireNuxtHubAuthorization } from '../../../utils/auth'
import { requireNuxtHubFeature } from '../../../utils/features'

export default eventHandler(async (event) => {
await requireNuxtHubAuthorization(event)
requireNuxtHubFeature('blob')

const { pathname } = await getValidatedRouterParams(event, z.object({
pathname: z.string().min(1)
}).parse)
Expand Down
5 changes: 5 additions & 0 deletions src/runtime/server/api/_hub/blob/[...pathname].get.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { eventHandler, getValidatedRouterParams } from 'h3'
import { z } from 'zod'
import { hubBlob } from '../../../utils/blob'
import { requireNuxtHubAuthorization } from '../../../utils/auth'
import { requireNuxtHubFeature } from '../../../utils/features'

export default eventHandler(async (event) => {
await requireNuxtHubAuthorization(event)
requireNuxtHubFeature('blob')

// TODO: handle caching in production
const { pathname } = await getValidatedRouterParams(event, z.object({
pathname: z.string().min(1)
Expand Down
5 changes: 5 additions & 0 deletions src/runtime/server/api/_hub/blob/[...pathname].put.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { eventHandler, getValidatedRouterParams, getHeader, getRequestWebStream, getQuery } from 'h3'
import { z } from 'zod'
import { hubBlob } from '../../../utils/blob'
import { requireNuxtHubAuthorization } from '../../../utils/auth'
import { requireNuxtHubFeature } from '../../../utils/features'

async function streamToArrayBuffer(stream: ReadableStream, streamSize: number) {
const result = new Uint8Array(streamSize)
Expand All @@ -19,6 +21,9 @@ async function streamToArrayBuffer(stream: ReadableStream, streamSize: number) {
}

export default eventHandler(async (event) => {
await requireNuxtHubAuthorization(event)
requireNuxtHubFeature('blob')

const { pathname } = await getValidatedRouterParams(event, z.object({
pathname: z.string().min(1)
}).parse)
Expand Down
5 changes: 5 additions & 0 deletions src/runtime/server/api/_hub/blob/delete.post.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { eventHandler, readValidatedBody, sendNoContent } from 'h3'
import { z } from 'zod'
import { hubBlob } from '../../../utils/blob'
import { requireNuxtHubAuthorization } from '../../../utils/auth'
import { requireNuxtHubFeature } from '../../../utils/features'

export default eventHandler(async (event) => {
await requireNuxtHubAuthorization(event)
requireNuxtHubFeature('blob')

const { pathnames } = await readValidatedBody(event, z.object({
pathnames: z.array(z.string().min(1)).min(1)
}).parse)
Expand Down
Loading

0 comments on commit a479ec8

Please sign in to comment.