',
+ )
+}
+
+function addHybridObjectsSection(contents) {
+ const match = contents.match(/^## Interfaces\n\n((?:- .+\n)+)/m)
+ if (!match) return contents
+
+ const entries = match[1].split('\n').filter(Boolean)
+ const hybridEntries = entries.filter((entry) =>
+ entry.includes('hybrid-objects/'),
+ )
+ if (hybridEntries.length === 0) return contents
+
+ const interfaceEntries = entries.filter(
+ (entry) => !entry.includes('hybrid-objects/'),
+ )
+ const withoutHybrids = contents.replace(
+ match[0],
+ interfaceEntries.length > 0
+ ? `## Interfaces\n\n${interfaceEntries.join('\n')}\n`
+ : '',
+ )
+ const firstGroup = withoutHybrids.search(
+ /^## (?:Enumerations|Classes|Interfaces|Type Aliases|Variables|Functions)\s*$/m,
+ )
+ if (firstGroup < 0) return withoutHybrids
+
+ return `${withoutHybrids.slice(0, firstGroup)}## Hybrid Objects\n\n${hybridEntries.join('\n')}\n\n${withoutHybrids.slice(firstGroup)}`
+}
+
+function addSourceHeader(page, contents) {
+ const { model } = page
+ const source = model.sources?.[0]
+ if (!source) return contents
+
+ const extension = source.fileName.match(
+ /\.(ts|tsx|js|jsx|cpp|cc|c|hpp|h|kt|swift)$/,
+ )?.[1]
+ const languages = {
+ ts: 'TypeScript',
+ tsx: 'TypeScript',
+ js: 'JavaScript',
+ jsx: 'JavaScript',
+ cpp: 'C++',
+ cc: 'C++',
+ c: 'C',
+ hpp: 'C++',
+ h: 'C++',
+ kt: 'Kotlin',
+ swift: 'Swift',
+ }
+ const language = languages[extension]
+ if (!language) return contents
+
+ const sourceUrl =
+ source.url ?? getSourceUrl(page, source.fileName, source.line)
+ const nativeLink = isHybridObject(model)
+ ? getNativeImplementationLink({ url: sourceUrl })
+ : undefined
+ const languagesInHeader = nativeLink ? `${language},C++` : language
+ const sourceLinks = [
+ `[${language}](${sourceUrl})`,
+ nativeLink ? `[C++](${nativeLink})` : undefined,
+ ].filter(Boolean)
+ const sourceLine = sourceLinks.length
+ ? `**Source files:** ${sourceLinks.join(' · ')}\n\n`
+ : ''
+
+ return contents
+ .replace(/^Defined in: [^\n]+\n\n/m, '')
+ .replace(
+ /^# (.+?)(\n\n)/m,
+ `
$2${sourceLine}`,
+ )
+}
+
+function styleSignatures(contents) {
+ return contents.replace(
+ /^> ([^\n]+)$/gm,
+ '
\n\n$1\n\n
',
+ )
+}
+
+function addMissingSourceLinks(page, contents) {
+ return contents.replace(
+ /^Defined in: ([^\n:]+):(\d+)$/gm,
+ (_, fileName, line) =>
+ `Defined in: [${fileName}:${line}](${getSourceUrl(page, fileName, line)})`,
+ )
+}
+
+function getSourceUrl(page, fileName, line) {
+ const packageName = page.url.split('/')[0]
+ const sourcePath = fileName.startsWith('src/') ? fileName : `src/${fileName}`
+
+ return `https://github.com/margelo/react-native-nitro-sqlite/blob/${gitRevision}/packages/${packageName}/${sourcePath}#L${line}`
+}
+
+function getNativeImplementationLink(source) {
+ const match = source.url?.match(
+ /^(https:\/\/github\.com\/margelo\/react-native-nitro-sqlite\/blob\/[^/]+)\/(packages\/[^/]+)\/src\/specs\/([^/]+)\.nitro\.ts#L\d+$/,
+ )
+ if (!match) return undefined
+
+ const [, baseUrl, packagePath, specName] = match
+ const nativePath = `${packagePath}/cpp/hybridObjects/Hybrid${specName}.hpp`
+ return existsSync(new URL(`../${nativePath}`, import.meta.url))
+ ? `${baseUrl}/${nativePath}`
+ : undefined
+}
diff --git a/docs/typedoc.json b/docs/typedoc.json
new file mode 100644
index 00000000..e2f9dda2
--- /dev/null
+++ b/docs/typedoc.json
@@ -0,0 +1,35 @@
+{
+ "entryPoints": [
+ "../packages/react-native-nitro-sqlite",
+ "../packages/react-native-nitro-sqlite-vec"
+ ],
+ "entryPointStrategy": "packages",
+ "name": "API Reference",
+ "packageOptions": {
+ "entryPoints": ["src/index.ts"],
+ "tsconfig": "../../docs/tsconfig.typedoc.json",
+ "readme": "none",
+ "excludeExternals": true,
+ "excludeInternal": true,
+ "excludePrivate": true,
+ "excludeProtected": true,
+ "disableSources": false,
+ "sourceLinkTemplate": "https://github.com/margelo/react-native-nitro-sqlite/blob/{gitRevision}/{path}#L{line}",
+ "sanitizeComments": true
+ },
+ "outputs": [
+ { "name": "markdown", "path": "content/api" },
+ { "name": "json", "path": "content/api-reflection.json" }
+ ],
+ "plugin": [
+ "typedoc-plugin-markdown",
+ "typedoc-plugin-frontmatter",
+ "./typedoc-fumadocs.mjs"
+ ],
+ "readme": "none",
+ "entryFileName": "index",
+ "router": "nitro-member",
+ "fileExtension": ".mdx",
+ "cleanOutputDir": true,
+ "useHTMLEncodedBrackets": true
+}
diff --git a/docs/vercel.json b/docs/vercel.json
new file mode 100644
index 00000000..6b4ffa6a
--- /dev/null
+++ b/docs/vercel.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "https://openapi.vercel.sh/vercel.json",
+ "framework": "nextjs",
+ "ignoreCommand": "if [ -n \"$VERCEL_GIT_PREVIOUS_SHA\" ] && cd .. && git diff --quiet \"$VERCEL_GIT_PREVIOUS_SHA\" HEAD -- docs packages config package.json bun.lock; then exit 0; else exit 1; fi",
+ "installCommand": "cd .. && bun install --frozen-lockfile"
+}
diff --git a/package.json b/package.json
index d9cd1630..9a256a75 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,8 @@
"workspaces": [
"packages/*",
"example",
- "example/macos"
+ "example/macos",
+ "docs"
],
"repository": {
"type": "git",
@@ -17,7 +18,7 @@
"bugs": {
"url": "https://github.com/margelo/react-native-nitro-sqlite/issues"
},
- "homepage": "https://github.com/margelo/react-native-nitro-sqlite#readme",
+ "homepage": "https://sqlite.margelo.com",
"scripts": {
"postinstall": "patch-package",
"typecheck": "tsc --build",
@@ -30,7 +31,8 @@
"test:release": "bun test scripts/sync-bun-lockfile.test.ts && node --test scripts/release.test.ts",
"release": "./scripts/release.sh",
"sqlite": "bun --cwd packages/react-native-nitro-sqlite",
- "example": "bun --cwd example"
+ "example": "bun --cwd example",
+ "docs": "bun --cwd docs"
},
"engines": {
"node": ">=22.13.0"
diff --git a/packages/react-native-nitro-sqlite/package.json b/packages/react-native-nitro-sqlite/package.json
index 4e9d6d00..0aa9a5a2 100644
--- a/packages/react-native-nitro-sqlite/package.json
+++ b/packages/react-native-nitro-sqlite/package.json
@@ -63,7 +63,7 @@
"bugs": {
"url": "https://github.com/margelo/react-native-nitro-sqlite/issues"
},
- "homepage": "https://github.com/margelo/react-native-nitro-sqlite#readme",
+ "homepage": "https://sqlite.margelo.com",
"publishConfig": {
"registry": "https://registry.npmjs.org/"
},
diff --git a/packages/react-native-nitro-sqlite/src/NitroSQLiteError.ts b/packages/react-native-nitro-sqlite/src/NitroSQLiteError.ts
index b2cfce4b..2eaf6bb1 100644
--- a/packages/react-native-nitro-sqlite/src/NitroSQLiteError.ts
+++ b/packages/react-native-nitro-sqlite/src/NitroSQLiteError.ts
@@ -1,11 +1,26 @@
const NITRO_SQLITE_ERROR_NAME = 'NitroSQLiteError' as const
+const NATIVE_EXCEPTION_PREFIX = '[NativeNitroSQLiteException]['
+
+/** Category attached to an error thrown by NitroSQLite's native implementation. */
+export type NitroSQLiteExceptionType =
+ | 'UnknownError'
+ | 'DatabaseCannotBeOpened'
+ | 'DatabaseNotOpen'
+ | 'UnableToAttachToDatabase'
+ | 'SqlExecutionError'
+ | 'CouldNotLoadFile'
+ | 'NoBatchCommandsProvided'
/** Error thrown by managed NitroSQLite operations. Native errors are wrapped with this class. */
export default class NitroSQLiteError extends Error {
+ /** Native exception category, if this error originated in NitroSQLite's C++ code. */
+ readonly type: NitroSQLiteExceptionType | undefined
+
/** Create an error with a message and optional cause. */
constructor(message: string, options?: ErrorOptions) {
super(message, options)
this.name = NITRO_SQLITE_ERROR_NAME
+ this.type = getNativeExceptionType(message)
// Maintains proper prototype chain for instanceof checks
Object.setPrototypeOf(this, NitroSQLiteError.prototype)
@@ -42,3 +57,28 @@ export default class NitroSQLiteError extends Error {
})
}
}
+
+function getNativeExceptionType(
+ message: string,
+): NitroSQLiteExceptionType | undefined {
+ const prefixStart = message.indexOf(NATIVE_EXCEPTION_PREFIX)
+ if (prefixStart === -1) return undefined
+
+ const typeStart = prefixStart + NATIVE_EXCEPTION_PREFIX.length
+ const typeEnd = message.indexOf(']', typeStart)
+ if (typeEnd === -1) return undefined
+
+ const type = message.slice(typeStart, typeEnd)
+ switch (type) {
+ case 'UnknownError':
+ case 'DatabaseCannotBeOpened':
+ case 'DatabaseNotOpen':
+ case 'UnableToAttachToDatabase':
+ case 'SqlExecutionError':
+ case 'CouldNotLoadFile':
+ case 'NoBatchCommandsProvided':
+ return type
+ default:
+ return undefined
+ }
+}
diff --git a/packages/react-native-nitro-sqlite/src/__tests__/NitroSQLiteError.test.ts b/packages/react-native-nitro-sqlite/src/__tests__/NitroSQLiteError.test.ts
index 6e45de09..7da94539 100644
--- a/packages/react-native-nitro-sqlite/src/__tests__/NitroSQLiteError.test.ts
+++ b/packages/react-native-nitro-sqlite/src/__tests__/NitroSQLiteError.test.ts
@@ -9,6 +9,7 @@ describe('NitroSQLiteError', () => {
expect(error).toBeInstanceOf(NitroSQLiteError)
expect(error.name).toBe('NitroSQLiteError')
expect(error.cause).toBe(cause)
+ expect(error.type).toBeUndefined()
})
it('returns an existing NitroSQLiteError unchanged', () => {
@@ -42,4 +43,45 @@ describe('NitroSQLiteError', () => {
expect(converted.message).toBe('Unknown error occurred')
expect(converted.cause).toBe(value)
})
+
+ it.each([
+ 'UnknownError',
+ 'DatabaseCannotBeOpened',
+ 'DatabaseNotOpen',
+ 'UnableToAttachToDatabase',
+ 'SqlExecutionError',
+ 'CouldNotLoadFile',
+ 'NoBatchCommandsProvided',
+ ])('exposes the %s native exception category', (type) => {
+ const original = new Error(
+ `Exception in HostFunction: [NativeNitroSQLiteException][${type}] failed`,
+ )
+ original.stack = 'native stack'
+
+ const converted = NitroSQLiteError.fromError(original)
+
+ expect(converted.type).toBe(type)
+ expect(converted.message).toBe(original.message)
+ expect(converted.stack).toBe(original.stack)
+ })
+
+ it('does not assign a native category for unrecognized exception types', () => {
+ const error = NitroSQLiteError.fromError(
+ '[NativeNitroSQLiteException][FutureError] failed',
+ )
+
+ expect(error.type).toBeUndefined()
+ expect(error.message).toBe(
+ '[NativeNitroSQLiteException][FutureError] failed',
+ )
+ })
+
+ it('ignores an incomplete native exception category', () => {
+ const error = NitroSQLiteError.fromError(
+ '[NativeNitroSQLiteException][SqlExecutionError',
+ )
+
+ expect(error.type).toBeUndefined()
+ expect(error.message).toBe('[NativeNitroSQLiteException][SqlExecutionError')
+ })
})
diff --git a/packages/react-native-nitro-sqlite/src/index.ts b/packages/react-native-nitro-sqlite/src/index.ts
index d79fac7a..1985c928 100644
--- a/packages/react-native-nitro-sqlite/src/index.ts
+++ b/packages/react-native-nitro-sqlite/src/index.ts
@@ -27,5 +27,14 @@ export const NitroSQLite = {
export { open } from './operations/session'
export { default as NitroSQLiteError } from './NitroSQLiteError'
+export type { NitroSQLiteExceptionType } from './NitroSQLiteError'
+export type { DatabaseQueueKey } from './DatabaseQueue'
+export type { NitroSQLite as NitroSQLiteNative } from './specs/NitroSQLite.nitro'
+export type { NitroSQLitePreparedStatement } from './specs/NitroSQLitePreparedStatement.nitro'
+export type {
+ NitroSQLiteQueryResult,
+ NitroSQLiteQueryColumnMetadata,
+} from './specs/NitroSQLiteQueryResult.nitro'
+export type { TypeOrmNitroSQLiteConnection } from './typeORM'
export type * from './types'
export { typeORMDriver } from './typeORM'
diff --git a/packages/react-native-nitro-sqlite/src/typeORM.ts b/packages/react-native-nitro-sqlite/src/typeORM.ts
index d981a9b3..16180288 100644
--- a/packages/react-native-nitro-sqlite/src/typeORM.ts
+++ b/packages/react-native-nitro-sqlite/src/typeORM.ts
@@ -14,7 +14,7 @@ import type {
import * as Operations from './operations/session'
/** Callback-oriented connection returned to TypeORM. */
-interface TypeOrmNitroSQLiteConnection {
+export interface TypeOrmNitroSQLiteConnection {
/** Execute SQL asynchronously and report the result through a callback. */
executeSql:
(
sql: string,