Skip to content

Commit e97d579

Browse files
authoredMar 18, 2025··
feat(db): experimental node:sqlite under flag (#3230)
1 parent 7549f53 commit e97d579

File tree

9 files changed

+99
-36
lines changed

9 files changed

+99
-36
lines changed
 

‎docs/nuxt.config.ts

+3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ export default defineNuxtConfig({
3232
},
3333

3434
content: {
35+
experimental: {
36+
nativeSqlite: true,
37+
},
3538
build: {
3639
markdown: {
3740
toc: {

‎playground/nuxt.config.ts

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ export default defineNuxtConfig({
55
'@nuxthub/core',
66
],
77
content: {
8+
experimental: {
9+
nativeSqlite: true,
10+
},
811
build: {
912
markdown: {
1013
remarkPlugins: {

‎src/module.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ export default defineNuxtModule<ModuleOptions>({
8181
json: true,
8282
},
8383
},
84+
experimental: {
85+
nativeSqlite: false,
86+
},
8487
},
8588
async setup(options, nuxt) {
8689
const resolver = createResolver(import.meta.url)
@@ -162,9 +165,10 @@ export default defineNuxtModule<ModuleOptions>({
162165
const preset = findPreset(nuxt)
163166
await preset.setupNitro(config, { manifest, resolver, moduleOptions: options })
164167

168+
const resolveOptions = { resolver, nativeSqlite: options.experimental?.nativeSqlite }
165169
config.alias ||= {}
166-
config.alias['#content/adapter'] = resolveDatabaseAdapter(config.runtimeConfig!.content!.database?.type || options.database.type, resolver)
167-
config.alias['#content/local-adapter'] = resolveDatabaseAdapter(options._localDatabase!.type || 'sqlite', resolver)
170+
config.alias['#content/adapter'] = resolveDatabaseAdapter(config.runtimeConfig!.content!.database?.type || options.database.type, resolveOptions)
171+
config.alias['#content/local-adapter'] = resolveDatabaseAdapter(options._localDatabase!.type || 'sqlite', resolveOptions)
168172

169173
config.handlers ||= []
170174
config.handlers.push({
@@ -230,7 +234,7 @@ async function processCollectionItems(nuxt: Nuxt, collections: ResolvedCollectio
230234
const collectionDump: Record<string, string[]> = {}
231235
const collectionChecksum: Record<string, string> = {}
232236
const collectionChecksumStructure: Record<string, string> = {}
233-
const db = await getLocalDatabase(options._localDatabase)
237+
const db = await getLocalDatabase(options._localDatabase, { nativeSqlite: options.experimental?.nativeSqlite })
234238
const databaseContents = await db.fetchDevelopmentCache()
235239

236240
const configHash = hash({

‎src/types/module.ts

+10
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,16 @@ export interface ModuleOptions {
179179
delimeter?: string
180180
}
181181
}
182+
183+
experimental?: {
184+
/**
185+
* Use Node.js native SQLite bindings instead of `better-sqlite3` if available
186+
* Node.js SQLite introduced in v22.5.0
187+
*
188+
* @default false
189+
*/
190+
nativeSqlite?: boolean
191+
}
182192
}
183193

184194
export interface RuntimeConfig {

‎src/utils/database.ts

+71-28
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,6 @@ import type { CacheEntry, D1DatabaseConfig, LocalDevelopmentDatabase, SqliteData
88
import type { ModuleOptions } from '../types/module'
99
import { logger } from './dev'
1010

11-
function isSqlite3Available() {
12-
if (!isWebContainer()) {
13-
return false
14-
}
15-
16-
try {
17-
// eslint-disable-next-line @typescript-eslint/no-require-imports
18-
require('sqlite3')
19-
return true
20-
}
21-
catch {
22-
logger.error('Nuxt Content requires `sqlite3` module to work in WebContainer environment. Please run `npm install sqlite3` to install it and try again.')
23-
process.exit(1)
24-
}
25-
}
26-
2711
export async function refineDatabaseConfig(database: ModuleOptions['database'], opts: { rootDir: string, updateSqliteFileName?: boolean }) {
2812
if (database.type === 'd1') {
2913
if (!('bindingName' in database)) {
@@ -44,14 +28,11 @@ export async function refineDatabaseConfig(database: ModuleOptions['database'],
4428
}
4529
}
4630

47-
export function getDefaultSqliteAdapter() {
48-
return process.versions.bun ? 'bunsqlite' : 'sqlite'
49-
}
50-
51-
export function resolveDatabaseAdapter(adapter: 'sqlite' | 'bunsqlite' | 'postgres' | 'libsql' | 'd1', resolver: Resolver) {
31+
export function resolveDatabaseAdapter(adapter: 'sqlite' | 'bunsqlite' | 'postgres' | 'libsql' | 'd1' | 'nodesqlite', opts: { resolver: Resolver, nativeSqlite?: boolean }) {
5232
const databaseConnectors = {
53-
sqlite: isSqlite3Available() ? 'db0/connectors/sqlite3' : 'db0/connectors/better-sqlite3',
54-
bunsqlite: resolver.resolve('./runtime/internal/connectors/bunsqlite'),
33+
sqlite: findBestSqliteAdapter({ nativeSqlite: opts.nativeSqlite }),
34+
nodesqlite: 'db0/connectors/node-sqlite',
35+
bunsqlite: opts.resolver.resolve('./runtime/internal/connectors/bunsqlite'),
5536
postgres: 'db0/connectors/postgresql',
5637
libsql: 'db0/connectors/libsql/web',
5738
d1: 'db0/connectors/cloudflare-d1',
@@ -65,23 +46,22 @@ export function resolveDatabaseAdapter(adapter: 'sqlite' | 'bunsqlite' | 'postgr
6546
return databaseConnectors[adapter]
6647
}
6748

68-
async function getDatabase(database: SqliteDatabaseConfig | D1DatabaseConfig): Promise<Connector> {
49+
async function getDatabase(database: SqliteDatabaseConfig | D1DatabaseConfig, opts: { nativeSqlite?: boolean }): Promise<Connector> {
6950
if (database.type === 'd1') {
7051
return cloudflareD1Connector({ bindingName: database.bindingName })
7152
}
7253

73-
const type = getDefaultSqliteAdapter()
74-
return import(type === 'bunsqlite' ? 'db0/connectors/bun-sqlite' : (isSqlite3Available() ? 'db0/connectors/sqlite3' : 'db0/connectors/better-sqlite3'))
54+
return import(findBestSqliteAdapter(opts))
7555
.then((m) => {
7656
const connector = (m.default || m) as (config: unknown) => Connector
7757
return connector({ path: database.filename })
7858
})
7959
}
8060

8161
const _localDatabase: Record<string, Connector> = {}
82-
export async function getLocalDatabase(database: SqliteDatabaseConfig | D1DatabaseConfig, connector?: Connector): Promise<LocalDevelopmentDatabase> {
62+
export async function getLocalDatabase(database: SqliteDatabaseConfig | D1DatabaseConfig, { connector, nativeSqlite }: { connector?: Connector, nativeSqlite?: boolean } = {}): Promise<LocalDevelopmentDatabase> {
8363
const databaseLocation = database.type === 'sqlite' ? database.filename : database.bindingName
84-
const db = _localDatabase[databaseLocation] || connector || await getDatabase(database)
64+
const db = _localDatabase[databaseLocation] || connector || await getDatabase(database, { nativeSqlite })
8565

8666
_localDatabase[databaseLocation] = db
8767
await db.exec('CREATE TABLE IF NOT EXISTS _development_cache (id TEXT PRIMARY KEY, checksum TEXT, parsedContent TEXT)')
@@ -128,3 +108,66 @@ export async function getLocalDatabase(database: SqliteDatabaseConfig | D1Databa
128108
dropContentTables,
129109
}
130110
}
111+
112+
function findBestSqliteAdapter(opts: { nativeSqlite?: boolean }) {
113+
if (process.versions.bun) {
114+
return 'db0/connectors/bun-sqlite'
115+
}
116+
117+
// if node:sqlite is available, use it
118+
if (opts.nativeSqlite && isNodeSqliteAvailable()) {
119+
return 'db0/connectors/node-sqlite'
120+
}
121+
122+
return isSqlite3Available() ? 'db0/connectors/sqlite3' : 'db0/connectors/better-sqlite3'
123+
}
124+
125+
function isNodeSqliteAvailable() {
126+
try {
127+
const module = globalThis.process?.getBuiltinModule?.('node:sqlite')
128+
129+
if (module) {
130+
// When using the SQLite Node.js prints warnings about the experimental feature
131+
// This is workaround to surpass the SQLite warning
132+
// Inspired by Yarn https://github.com/yarnpkg/berry/blob/182046546379f3b4e111c374946b32d92be5d933/packages/yarnpkg-pnp/sources/loader/applyPatch.ts#L307-L328
133+
const originalEmit = process.emit
134+
// @ts-expect-error - TS complains about the return type of originalEmit.apply
135+
process.emit = function (...args) {
136+
const name = args[0]
137+
const data = args[1] as { name: string, message: string }
138+
if (
139+
name === `warning`
140+
&& typeof data === `object`
141+
&& data.name === `ExperimentalWarning`
142+
&& data.message.includes(`SQLite is an experimental feature`)
143+
) {
144+
return false
145+
}
146+
return originalEmit.apply(process, args as unknown as Parameters<typeof process.emit>)
147+
}
148+
149+
return true
150+
}
151+
152+
return false
153+
}
154+
catch {
155+
return false
156+
}
157+
}
158+
159+
function isSqlite3Available() {
160+
if (!isWebContainer()) {
161+
return false
162+
}
163+
164+
try {
165+
// eslint-disable-next-line @typescript-eslint/no-require-imports
166+
require('sqlite3')
167+
return true
168+
}
169+
catch {
170+
logger.error('Nuxt Content requires `sqlite3` module to work in WebContainer environment. Please run `npm install sqlite3` to install it and try again.')
171+
process.exit(1)
172+
}
173+
}

‎src/utils/dev.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { parseSourceBase } from './source'
2222
export const logger: ConsolaInstance = useLogger('@nuxt/content')
2323

2424
export async function startSocketServer(nuxt: Nuxt, options: ModuleOptions, manifest: Manifest) {
25-
const db = await getLocalDatabase(options._localDatabase)
25+
const db = await getLocalDatabase(options._localDatabase, { nativeSqlite: options.experimental?.nativeSqlite })
2626

2727
let websocket: ReturnType<typeof createWebSocket>
2828
let listener: Listener
@@ -89,7 +89,7 @@ export async function startSocketServer(nuxt: Nuxt, options: ModuleOptions, mani
8989
export async function watchContents(nuxt: Nuxt, options: ModuleOptions, manifest: Manifest, socket: Awaited<ReturnType<typeof startSocketServer>>) {
9090
const collectionParsers = {} as Record<string, Awaited<ReturnType<typeof createParser>>>
9191

92-
const db = await getLocalDatabase(options._localDatabase!)
92+
const db = await getLocalDatabase(options._localDatabase!, { nativeSqlite: options.experimental?.nativeSqlite })
9393
const collections = manifest.collections
9494

9595
const sourceMap = collections.flatMap((c) => {

‎test/basic.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ describe('basic', async () => {
5858
})
5959

6060
test('load database', async () => {
61-
db = await getLocalDatabase({ type: 'sqlite', filename: fileURLToPath(new URL('./fixtures/basic/.data/content/contents.sqlite', import.meta.url)) })
61+
db = await getLocalDatabase({ type: 'sqlite', filename: fileURLToPath(new URL('./fixtures/basic/.data/content/contents.sqlite', import.meta.url)) }, { nativeSqlite: true })
6262
})
6363

6464
test('content table is created', async () => {

‎test/bun.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ describe('Local database', () => {
1414
})
1515

1616
test('load database', async () => {
17-
db = await getLocalDatabase({ type: 'sqlite', filename: ':memory:' })
17+
db = await getLocalDatabase({ type: 'sqlite', filename: ':memory:' }, { nativeSqlite: true })
1818
expect(db).toBeDefined()
1919
})
2020

‎test/empty.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ describe('empty', async () => {
6363
})
6464

6565
test('load database', async () => {
66-
db = await getLocalDatabase({ type: 'sqlite', filename: fileURLToPath(new URL('./fixtures/empty/.data/content/contents.sqlite', import.meta.url)) })
66+
db = await getLocalDatabase({ type: 'sqlite', filename: fileURLToPath(new URL('./fixtures/empty/.data/content/contents.sqlite', import.meta.url)) }, { nativeSqlite: true })
6767
})
6868

6969
test('content table is created', async () => {

0 commit comments

Comments
 (0)
Please sign in to comment.