Skip to content

Commit

Permalink
feat(database): add support for BunSQLite (#2944)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Farnabaz <[email protected]>
  • Loading branch information
felixrydberg and farnabaz authored Jan 7, 2025
1 parent 71036e2 commit db77463
Show file tree
Hide file tree
Showing 15 changed files with 291 additions and 108 deletions.
15 changes: 15 additions & 0 deletions docs/content/docs/1.getting-started/3.configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Configure markdown parser.
#### `toc`

::code-group

```ts [Default]
toc: {
depth: 2,
Expand All @@ -37,6 +38,7 @@ type Toc = {
searchDepth: number
}
```
::
Control behavior of Table of Contents generation.
Expand All @@ -63,13 +65,15 @@ export default defineNuxtConfig({
#### `remarkPlugins`

::code-group

```ts [Default]
remarkPlugins: {}
```

```ts [Signature]
type RemarkPlugins = Record<string, false | MarkdownPlugin>
```
::
A list of [remark](https://github.com/remarkjs/remark) plugins to use.
Expand Down Expand Up @@ -101,13 +105,15 @@ export default defineNuxtConfig({
#### `rehypePlugins`

::code-group

```ts [Default]
rehypePlugins: {}
```

```ts [Signature]
type RehypePlugins = object
```
::
A list of [rehype](https://github.com/remarkjs/remark-rehype) plugins to use.
Expand All @@ -132,13 +138,15 @@ export default defineNuxtConfig({
#### `highlight`

::code-group

```ts [Default]
highlight: false
```

```ts [Signature]
type Highlight = false | object
```
::
Nuxt Content uses [Shiki](https://github.com/shikijs/shiki) to provide syntax highlighting for [`ProsePre`](/docs/components/prose#prosepre) and [`ProseCode`](/docs/components/prose#prosecode).
Expand Down Expand Up @@ -257,6 +265,7 @@ Here is the list of supported database adapters:
### `SQLite`

If you want to change the default database location and move it to elsewhere you can use `sqlite` adapter to do so. This is the default value to the `database` option.
Depending on your runtime-environment different sqlite adapters will be used (Node: better-sqlite-3, Bun: bun:sqlite).

```ts [nuxt.config.ts]
export default defineNuxtConfig({
Expand Down Expand Up @@ -343,13 +352,15 @@ Configure content renderer.
### `anchorLinks`

::code-group

```ts [Default]
{ h2: true, h3: true, h4: true }
```

```ts [Signature]
type AnchorLinks = boolean | Record<'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6', boolean>
```
::
Control anchor link generation, by default it generates anchor links for `h2`, `h3` and `h4` heading
Expand All @@ -369,13 +380,15 @@ Value:
### `alias`
::code-group
```ts [Default]
alias: {}
```
```ts [Signature]
type Alias = Record<string, string>
```
::
Aliases will be used to replace markdown components and render custom components instead of default ones.
Expand Down Expand Up @@ -414,6 +427,7 @@ The watcher is a development feature and will not be included in production.
::

::code-group

```ts [Enabled]
export default defineNuxtConfig({
content: {
Expand All @@ -436,4 +450,5 @@ export default defineNuxtConfig({
}
})
```

::
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"release": "npm run lint && npm run test && npm run prepack && release-it",
"lint": "eslint .",
"test": "vitest run",
"test:bun": "bun test ./test/bun.test.ts",
"test:watch": "vitest watch",
"test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit",
"verify": "npm run dev:prepare && npm run lint && npm run test && npm run typecheck"
Expand Down Expand Up @@ -107,6 +108,7 @@
"@nuxt/test-utils": "^3.15.1",
"@release-it/conventional-changelog": "^9.0.4",
"@types/better-sqlite3": "^7.6.12",
"@types/bun": "^1.1.14",
"@types/micromatch": "^4.0.9",
"@types/minimatch": "^5.1.2",
"@types/node": "^22.10.5",
Expand All @@ -133,7 +135,9 @@
"./src/studio"
],
"externals": [
"untyped"
"untyped",
"bun:sqlite",
"bun:test"
],
"rollup": {
"output": {
Expand Down
25 changes: 24 additions & 1 deletion pnpm-lock.yaml

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

10 changes: 4 additions & 6 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ import { generateCollectionInsert, generateCollectionTableDefinition } from './u
import { componentsManifestTemplate, contentTypesTemplate, fullDatabaseRawDumpTemplate, manifestTemplate, moduleTemplates } from './utils/templates'
import type { ResolvedCollection } from './types/collection'
import type { ModuleOptions, SqliteDatabaseConfig } from './types/module'
import { getContentChecksum, localDatabase, logger, watchContents, chunks, watchComponents, startSocketServer } from './utils/dev'
import { getContentChecksum, logger, watchContents, chunks, watchComponents, startSocketServer } from './utils/dev'
import { loadContentConfig } from './utils/config'
import { createParser } from './utils/content'
import { installMDCModule } from './utils/mdc'
import { findPreset } from './presets'
import type { Manifest } from './types/manifest'
import { setupStudio } from './utils/studio/module'
import { parseSourceBase } from './utils/source'
import { getLocalDatabase } from './utils/sqlite'

// Export public utils
export * from './utils'
Expand Down Expand Up @@ -103,7 +104,6 @@ export default defineNuxtModule<ModuleOptions>({
(options.database as SqliteDatabaseConfig).filename = (options.database as SqliteDatabaseConfig).filename
await mkdir(dirname((options.database as SqliteDatabaseConfig).filename), { recursive: true }).catch(() => {})
}

const { collections } = await loadContentConfig(nuxt)
manifest.collections = collections

Expand Down Expand Up @@ -186,7 +186,6 @@ export default defineNuxtModule<ModuleOptions>({
if (nuxt.options._prepare) {
return
}

const dumpGeneratePromise = processCollectionItems(nuxt, manifest.collections, options)
.then((fest) => {
manifest.checksum = fest.checksum
Expand Down Expand Up @@ -234,8 +233,8 @@ export default defineNuxtModule<ModuleOptions>({
async function processCollectionItems(nuxt: Nuxt, collections: ResolvedCollection[], options: ModuleOptions) {
const collectionDump: Record<string, string[]> = {}
const collectionChecksum: Record<string, string> = {}
const db = localDatabase(options._localDatabase!.filename)
const databaseContents = db.fetchDevelopmentCache()
const db = await getLocalDatabase(options._localDatabase!.filename)
const databaseContents = await db.fetchDevelopmentCache()

const configHash = hash({
mdcHighlight: (nuxt.options as unknown as { mdc: MDCModuleOptions }).mdc?.highlight,
Expand All @@ -251,7 +250,6 @@ async function processCollectionItems(nuxt: Nuxt, collections: ResolvedCollectio

// Remove all existing content collections to start with a clean state
db.dropContentTables()

// Create database dump
for await (const collection of collections) {
if (collection.name === 'info') {
Expand Down
26 changes: 6 additions & 20 deletions src/runtime/adapters/sqlite.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,11 @@
import Database from 'better-sqlite3'
import { isAbsolute } from 'pathe'
import { createDatabaseAdapter } from '../internal/database-adapter'
import { getBetter3DatabaseAdapter } from '../internal/sqlite'
import { getBunSqliteDatabaseAdapter } from '../internal/bunsqlite'

let db: Database.Database
export default createDatabaseAdapter<{ filename: string }>((opts) => {
if (!db) {
const filename = !opts || isAbsolute(opts?.filename || '')
? opts?.filename
: new URL(opts.filename, (globalThis as unknown as { _importMeta_: { url: string } })._importMeta_.url).pathname
db = new Database(process.platform === 'win32' ? filename.slice(1) : filename)
}

return {
async all<T>(sql: string, params?: Array<number | string | boolean>): Promise<T[]> {
return params ? db.prepare<unknown[], T>(sql).all(params) : db.prepare<unknown[], T>(sql).all()
},
async first<T>(sql: string, params?: Array<number | string | boolean>) {
return params ? db.prepare<unknown[], T>(sql).get(params) : db.prepare<unknown[], T>(sql).get()
},
async exec(sql: string): Promise<void> {
await db.exec(sql)
},
// NOTE: Not using the getDefaultSqliteAdapter function here because its not in the runtime directory.
if (process.versions.bun) {
return getBunSqliteDatabaseAdapter(opts)
}
return getBetter3DatabaseAdapter(opts)
})
72 changes: 72 additions & 0 deletions src/runtime/internal/bunsqlite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import * as zlib from 'node:zlib'
import { isAbsolute } from 'pathe'
import type { Database as BunDatabaseType } from 'bun:sqlite'

/**
* CompressionStream and DecompressionStream polyfill for Bun
*/
if (!globalThis.CompressionStream) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const make = (ctx: unknown, handle: any) => Object.assign(ctx, {
writable: new WritableStream({
write: (chunk: ArrayBufferView) => handle.write(chunk),
close: () => handle.end(),
}),
readable: new ReadableStream({
type: 'bytes',
start(ctrl) {
handle.on('data', (chunk: ArrayBufferView) => ctrl.enqueue(chunk))
handle.once('end', () => ctrl.close())
},
}),
})

// eslint-disable-next-line @typescript-eslint/no-extraneous-class
globalThis.CompressionStream = class CompressionStream {
constructor(format: string) {
make(this, format === 'deflate'
? zlib.createDeflate()
: format === 'gzip' ? zlib.createGzip() : zlib.createDeflateRaw())
}
} as unknown as typeof CompressionStream

// eslint-disable-next-line @typescript-eslint/no-extraneous-class
globalThis.DecompressionStream = class DecompressionStream {
constructor(format: string) {
make(this, format === 'deflate'
? zlib.createInflate()
: format === 'gzip'
? zlib.createGunzip()
: zlib.createInflateRaw())
}
} as unknown as typeof DecompressionStream
}

function getBunDatabaseSync() {
// A top level import will make Nuxt complain about a missing module
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('bun:sqlite').Database
}

let db: BunDatabaseType
export const getBunSqliteDatabaseAdapter = (opts: { filename: string }) => {
const Database = getBunDatabaseSync()
if (!db) {
const filename = !opts || isAbsolute(opts?.filename || '') || opts?.filename === ':memory:'
? opts?.filename
: new URL(opts.filename, (globalThis as unknown as { _importMeta_: { url: string } })._importMeta_.url).pathname
db = new Database(filename, { create: true })
}

return {
async all<T>(sql: string, params?: Array<number | string | boolean>): Promise<T[]> {
return params ? db.prepare(sql).all(...params) as T[] : db.prepare(sql).all() as T[]
},
async first<T>(sql: string, params?: Array<number | string | boolean>): Promise<T | null> {
return params ? db.prepare(sql).get(...params) as T : db.prepare(sql).get() as T
},
async exec(sql: string): Promise<unknown> {
return db.prepare(sql).run()
},
}
}
Loading

0 comments on commit db77463

Please sign in to comment.