Merge 'Fix JavaScript bindings packaging' from Nikita Sivukhin

This PR configure `#entry-point` import alias for javascript bindings in
order to use `browser.js` napi-rs generated file in browser context.
Also, this PR forces napi-rs to emit `index.js` entrypoint using ESM and
also use typescript for writing our wrapper code around napi-rs
bindings.
In order to make behaviour consistent when lib is imported through ESM
or CommonJS this PR also replace default export of `Database` by named
on. The problem is that `export default Database` will be logically
equivalent to `modules.export.default = Database` which is not the same
thing as `modules.export = Database` and this will need to access
additional `.default` field with CommonJs style imports (e.g. `new
require('@tursodatabase/turso').default(...)`). In order to remove this
difference - I just replaced default export with named one.

Closes #2488
This commit is contained in:
Pekka Enberg
2025-08-08 10:42:21 +03:00
committed by GitHub
17 changed files with 207 additions and 128 deletions

View File

@@ -8,7 +8,7 @@ repository.workspace = true
description = "The Turso database library Node bindings"
[lib]
crate-type = ["cdylib"]
crate-type = ["cdylib", "lib"]
[dependencies]
turso_core = { workspace = true }

View File

@@ -7,7 +7,7 @@
// The `params` parameter is an array of parameters.
//
// The function returns void.
function bindParams(stmt, params) {
export function bindParams(stmt, params) {
const len = params?.length;
if (len === 0) {
return;
@@ -65,6 +65,4 @@ function bindPositionalParams(stmt, params) {
function bindValue(stmt, index, value) {
stmt.bindAt(index, value);
}
module.exports = { bindParams };
}

View File

@@ -1,6 +1,6 @@
import { drizzle } from "drizzle-orm/better-sqlite3";
import { sql } from "drizzle-orm";
import Database from '@tursodatabase/turso';
import { Database } from '@tursodatabase/turso';
const sqlite = new Database('sqlite.db');
const db = drizzle({ client: sqlite });

View File

@@ -2,6 +2,7 @@
"name": "drizzle",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},

View File

@@ -3,8 +3,9 @@
// @ts-nocheck
/* auto-generated by NAPI-RS */
const { createRequire } = require('node:module')
require = createRequire(__filename)
import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)
const __dirname = new URL('.', import.meta.url).pathname
const { readFileSync } = require('node:fs')
let nativeBinding = null
@@ -392,6 +393,6 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`)
}
module.exports = nativeBinding
module.exports.Database = nativeBinding.Database
module.exports.Statement = nativeBinding.Statement
const { Database, Statement } = nativeBinding
export { Database }
export { Statement }

View File

@@ -8,13 +8,12 @@
"name": "@tursodatabase/turso",
"version": "0.1.4-pre.2",
"license": "MIT",
"dependencies": {
"@napi-rs/wasm-runtime": "^1.0.1"
},
"devDependencies": {
"@napi-rs/cli": "^3.0.4",
"@napi-rs/wasm-runtime": "^1.0.1",
"ava": "^6.0.1",
"better-sqlite3": "^11.9.1"
"better-sqlite3": "^11.9.1",
"typescript": "^5.9.2"
},
"engines": {
"node": ">= 10"
@@ -24,6 +23,7 @@
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz",
"integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@emnapi/wasi-threads": "1.0.4",
@@ -34,6 +34,7 @@
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz",
"integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==",
"dev": true,
"license": "MIT",
"dependencies": {
"tslib": "^2.4.0"
@@ -43,6 +44,7 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz",
"integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==",
"dev": true,
"license": "MIT",
"dependencies": {
"tslib": "^2.4.0"
@@ -1266,6 +1268,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.1.tgz",
"integrity": "sha512-KVlQ/jgywZpixGCKMNwxStmmbYEMyokZpCf2YuIChhfJA2uqfAKNEM8INz7zzTo55iEXfBhIIs3VqYyqzDLj8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@emnapi/core": "^1.4.5",
@@ -1774,6 +1777,7 @@
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz",
"integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"tslib": "^2.4.0"
@@ -4335,6 +4339,7 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD"
},
"node_modules/tunnel-agent": {
@@ -4373,6 +4378,20 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/typescript": {
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/unicorn-magic": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
@@ -4729,4 +4748,4 @@
}
}
}
}
}

View File

@@ -6,18 +6,17 @@
"url": "https://github.com/tursodatabase/turso"
},
"description": "The Turso database library",
"main": "promise.js",
"module": "./dist/promise.js",
"main": "./dist/promise.js",
"type": "module",
"exports": {
".": "./promise.js",
"./sync": "./sync.js"
".": "./dist/promise.js",
"./sync": "./dist/sync.js"
},
"files": [
"bind.js",
"browser.js",
"index.js",
"promise.js",
"sqlite-error.js",
"sync.js"
"dist/**"
],
"types": "index.d.ts",
"napi": {
@@ -34,7 +33,8 @@
"@napi-rs/cli": "^3.0.4",
"@napi-rs/wasm-runtime": "^1.0.1",
"ava": "^6.0.1",
"better-sqlite3": "^11.9.1"
"better-sqlite3": "^11.9.1",
"typescript": "^5.9.2"
},
"ava": {
"timeout": "3m"
@@ -44,12 +44,19 @@
},
"scripts": {
"artifacts": "napi artifacts",
"build": "napi build --platform --release",
"build:debug": "napi build --platform",
"build": "npm exec tsc && napi build --platform --release --esm",
"build:debug": "npm exec tsc && napi build --platform",
"prepublishOnly": "napi prepublish -t npm",
"test": "true",
"universal": "napi universalize",
"version": "napi version"
},
"packageManager": "yarn@4.9.2"
}
"packageManager": "yarn@4.9.2",
"imports": {
"#entry-point": {
"types": "./index.d.ts",
"browser": "./browser.js",
"node": "./index.js"
}
}
}

View File

@@ -1,9 +1,7 @@
"use strict";
import { Database as NativeDB, Statement as NativeStatement } from "#entry-point";
import { bindParams } from "./bind.js";
const { Database: NativeDB } = require("./index.js");
const { bindParams } = require("./bind.js");
const SqliteError = require("./sqlite-error.js");
import { SqliteError } from "./sqlite-error.js";
// Step result constants
const STEP_ROW = 1;
@@ -37,6 +35,9 @@ function createErrorByName(name, message) {
* Database represents a connection that can prepare and execute SQL statements.
*/
class Database {
db: NativeDB;
memory: boolean;
open: boolean;
/**
* Creates a new database connection. If the database file pointed to by `path` does not exists, it will be created.
*
@@ -47,30 +48,35 @@ class Database {
* @param {boolean} [opts.fileMustExist=false] - If true, throws if database file does not exist.
* @param {number} [opts.timeout=0] - Timeout duration in milliseconds for database operations. Defaults to 0 (no timeout).
*/
constructor(path, opts = {}) {
constructor(path: string, opts: any = {}) {
opts.readonly = opts.readonly === undefined ? false : opts.readonly;
opts.fileMustExist =
opts.fileMustExist === undefined ? false : opts.fileMustExist;
opts.timeout = opts.timeout === undefined ? 0 : opts.timeout;
this.db = new NativeDB(path, opts);
this.memory = this.db.memory;
const db = this.db;
const db = new NativeDB(path);
this.initialize(db, opts.path, opts.readonly);
}
static create() {
return Object.create(this.prototype);
}
initialize(db: NativeDB, name, readonly) {
this.db = db;
this.memory = db.memory;
Object.defineProperties(this, {
inTransaction: {
get() {
return db.inTransaction();
throw new Error("not implemented");
},
},
name: {
get() {
return path;
return name;
},
},
readonly: {
get() {
return opts.readonly;
return readonly;
},
},
open: {
@@ -90,7 +96,7 @@ class Database {
if (!this.open) {
throw new TypeError("The database connection is not open");
}
if (!sql) {
throw new RangeError("The supplied SQL string contains no statements");
}
@@ -149,10 +155,10 @@ class Database {
throw new TypeError("Expected second argument to be an options object");
const pragma = `PRAGMA ${source}`;
const stmt = this.prepare(pragma);
const results = stmt.all();
return results;
}
@@ -177,7 +183,7 @@ class Database {
}
loadExtension(path) {
this.db.loadExtension(path);
throw new Error("not implemented");
}
maxWriteReplicationIndex() {
@@ -193,7 +199,7 @@ class Database {
if (!this.open) {
throw new TypeError("The database connection is not open");
}
try {
this.db.batch(sql);
} catch (err) {
@@ -205,7 +211,7 @@ class Database {
* Interrupts the database connection.
*/
interrupt() {
this.db.interrupt();
throw new Error("not implemented");
}
/**
@@ -220,6 +226,8 @@ class Database {
* Statement represents a prepared SQL statement that can be executed.
*/
class Statement {
stmt: NativeStatement;
db: Database;
constructor(stmt, database) {
this.stmt = stmt;
this.db = database;
@@ -246,17 +254,13 @@ class Statement {
}
get source() {
return this.stmt.source;
throw new Error("not implemented");
}
get reader() {
throw new Error("not implemented");
}
get source() {
return this.stmt.source;
}
get database() {
return this.db;
}
@@ -266,10 +270,10 @@ class Statement {
*/
async run(...bindParameters) {
const totalChangesBefore = this.db.db.totalChanges();
this.stmt.reset();
bindParams(this.stmt, bindParameters);
while (true) {
const stepResult = this.stmt.step();
if (stepResult === STEP_IO) {
@@ -284,10 +288,10 @@ class Statement {
continue;
}
}
const lastInsertRowid = this.db.db.lastInsertRowid();
const changes = this.db.db.totalChanges() === totalChangesBefore ? 0 : this.db.db.changes();
return { changes, lastInsertRowid };
}
@@ -299,7 +303,7 @@ class Statement {
async get(...bindParameters) {
this.stmt.reset();
bindParams(this.stmt, bindParameters);
while (true) {
const stepResult = this.stmt.step();
if (stepResult === STEP_IO) {
@@ -323,7 +327,7 @@ class Statement {
async *iterate(...bindParameters) {
this.stmt.reset();
bindParams(this.stmt, bindParameters);
while (true) {
const stepResult = this.stmt.step();
if (stepResult === STEP_IO) {
@@ -347,8 +351,8 @@ class Statement {
async all(...bindParameters) {
this.stmt.reset();
bindParams(this.stmt, bindParameters);
const rows = [];
const rows: any[] = [];
while (true) {
const stepResult = this.stmt.step();
if (stepResult === STEP_IO) {
@@ -369,15 +373,14 @@ class Statement {
* Interrupts the statement.
*/
interrupt() {
this.stmt.interrupt();
return this;
throw new Error("not implemented");
}
/**
* Returns the columns in the result set returned by this prepared statement.
*/
columns() {
return this.stmt.columns();
throw new Error("not implemented");
}
/**
@@ -396,5 +399,4 @@ class Statement {
}
}
module.exports = Database;
module.exports.SqliteError = SqliteError;
export { Database, SqliteError }

View File

@@ -1,14 +0,0 @@
'use strict';
class SqliteError extends Error {
constructor(message, code, rawCode) {
super(message);
this.name = 'SqliteError';
this.code = code;
this.rawCode = rawCode;
Error.captureStackTrace(this, SqliteError);
}
}
module.exports = SqliteError;

View File

@@ -0,0 +1,13 @@
export class SqliteError extends Error {
name: string;
code: string;
rawCode: string;
constructor(message, code, rawCode) {
super(message);
this.name = 'SqliteError';
this.code = code;
this.rawCode = rawCode;
(Error as any).captureStackTrace(this, SqliteError);
}
}

View File

@@ -34,8 +34,9 @@ enum PresentationMode {
/// A database connection.
#[napi]
#[derive(Clone)]
pub struct Database {
_db: Arc<turso_core::Database>,
_db: Option<Arc<turso_core::Database>>,
io: Arc<dyn turso_core::IO>,
conn: Arc<turso_core::Connection>,
is_memory: bool,
@@ -76,13 +77,22 @@ impl Database {
.connect()
.map_err(|e| Error::new(Status::GenericFailure, format!("Failed to connect: {e}")))?;
Ok(Database {
Ok(Self::create(Some(db), io, conn, is_memory))
}
pub fn create(
db: Option<Arc<turso_core::Database>>,
io: Arc<dyn turso_core::IO>,
conn: Arc<turso_core::Connection>,
is_memory: bool,
) -> Self {
Database {
_db: db,
io,
conn,
is_memory,
is_open: Cell::new(true),
})
}
}
/// Returns whether the database is in memory-only mode.
@@ -418,7 +428,8 @@ impl Statement {
/// Async task for running the I/O loop.
pub struct IoLoopTask {
io: Arc<dyn turso_core::IO>,
// this field is set in the turso-sync-engine package
pub io: Arc<dyn turso_core::IO>,
}
impl Task for IoLoopTask {

View File

@@ -1,9 +1,7 @@
"use strict";
import { Database as NativeDB, Statement as NativeStatement } from "#entry-point";
import { bindParams } from "./bind.js";
const { Database: NativeDB } = require("./index.js");
const { bindParams } = require("./bind.js");
const SqliteError = require("./sqlite-error.js");
import { SqliteError } from "./sqlite-error.js";
// Step result constants
const STEP_ROW = 1;
@@ -37,6 +35,10 @@ function createErrorByName(name, message) {
* Database represents a connection that can prepare and execute SQL statements.
*/
class Database {
db: NativeDB;
memory: boolean;
open: boolean;
/**
* Creates a new database connection. If the database file pointed to by `path` does not exists, it will be created.
*
@@ -47,20 +49,20 @@ class Database {
* @param {boolean} [opts.fileMustExist=false] - If true, throws if database file does not exist.
* @param {number} [opts.timeout=0] - Timeout duration in milliseconds for database operations. Defaults to 0 (no timeout).
*/
constructor(path, opts = {}) {
constructor(path: string, opts: any = {}) {
opts.readonly = opts.readonly === undefined ? false : opts.readonly;
opts.fileMustExist =
opts.fileMustExist === undefined ? false : opts.fileMustExist;
opts.timeout = opts.timeout === undefined ? 0 : opts.timeout;
this.db = new NativeDB(path, opts);
this.db = new NativeDB(path);
this.memory = this.db.memory;
const db = this.db;
Object.defineProperties(this, {
inTransaction: {
get() {
return db.inTransaction();
throw new Error("not implemented");
},
},
name: {
@@ -90,7 +92,7 @@ class Database {
if (!this.open) {
throw new TypeError("The database connection is not open");
}
if (!sql) {
throw new RangeError("The supplied SQL string contains no statements");
}
@@ -149,10 +151,10 @@ class Database {
throw new TypeError("Expected second argument to be an options object");
const pragma = `PRAGMA ${source}`;
const stmt = this.prepare(pragma);
const results = stmt.all();
return results;
}
@@ -177,7 +179,7 @@ class Database {
}
loadExtension(path) {
this.db.loadExtension(path);
throw new Error("not implemented");
}
maxWriteReplicationIndex() {
@@ -193,7 +195,7 @@ class Database {
if (!this.open) {
throw new TypeError("The database connection is not open");
}
try {
this.db.batch(sql);
} catch (err) {
@@ -205,7 +207,7 @@ class Database {
* Interrupts the database connection.
*/
interrupt() {
this.db.interrupt();
throw new Error("not implemented");
}
/**
@@ -220,7 +222,10 @@ class Database {
* Statement represents a prepared SQL statement that can be executed.
*/
class Statement {
constructor(stmt, database) {
stmt: NativeStatement;
db: Database;
constructor(stmt: NativeStatement, database: Database) {
this.stmt = stmt;
this.db = database;
}
@@ -246,17 +251,13 @@ class Statement {
}
get source() {
return this.stmt.source;
throw new Error("not implemented");
}
get reader() {
throw new Error("not implemented");
}
get source() {
return this.stmt.source;
}
get database() {
return this.db;
}
@@ -266,10 +267,10 @@ class Statement {
*/
run(...bindParameters) {
const totalChangesBefore = this.db.db.totalChanges();
this.stmt.reset();
bindParams(this.stmt, bindParameters);
for (;;) {
for (; ;) {
const stepResult = this.stmt.step();
if (stepResult === STEP_IO) {
this.db.db.ioLoopSync();
@@ -283,10 +284,10 @@ class Statement {
continue;
}
}
const lastInsertRowid = this.db.db.lastInsertRowid();
const changes = this.db.db.totalChanges() === totalChangesBefore ? 0 : this.db.db.changes();
return { changes, lastInsertRowid };
}
@@ -298,7 +299,7 @@ class Statement {
get(...bindParameters) {
this.stmt.reset();
bindParams(this.stmt, bindParameters);
for (;;) {
for (; ;) {
const stepResult = this.stmt.step();
if (stepResult === STEP_IO) {
this.db.db.ioLoopSync();
@@ -321,7 +322,7 @@ class Statement {
*iterate(...bindParameters) {
this.stmt.reset();
bindParams(this.stmt, bindParameters);
while (true) {
const stepResult = this.stmt.step();
if (stepResult === STEP_IO) {
@@ -345,8 +346,8 @@ class Statement {
all(...bindParameters) {
this.stmt.reset();
bindParams(this.stmt, bindParameters);
const rows = [];
for (;;) {
const rows: any[] = [];
for (; ;) {
const stepResult = this.stmt.step();
if (stepResult === STEP_IO) {
this.db.db.ioLoopSync();
@@ -366,15 +367,14 @@ class Statement {
* Interrupts the statement.
*/
interrupt() {
this.stmt.interrupt();
return this;
throw new Error("not implemented");
}
/**
* Returns the columns in the result set returned by this prepared statement.
*/
columns() {
return this.stmt.columns();
throw new Error("not implemented");
}
/**
@@ -393,5 +393,4 @@ class Statement {
}
}
module.exports = Database;
module.exports.SqliteError = SqliteError;
export { Database, SqliteError }

View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"skipLibCheck": true,
"module": "esnext",
"target": "esnext",
"outDir": "dist/",
"lib": [
"es2020"
],
"paths": {
"#entry-point": [
"./index.js"
]
}
},
"include": [
"*"
]
}

View File

@@ -1076,6 +1076,7 @@ __metadata:
"@napi-rs/wasm-runtime": "npm:^1.0.1"
ava: "npm:^6.0.1"
better-sqlite3: "npm:^11.9.1"
typescript: "npm:^5.9.2"
languageName: unknown
linkType: soft
@@ -2491,8 +2492,8 @@ __metadata:
linkType: hard
"node-gyp@npm:latest":
version: 11.2.0
resolution: "node-gyp@npm:11.2.0"
version: 11.3.0
resolution: "node-gyp@npm:11.3.0"
dependencies:
env-paths: "npm:^2.2.0"
exponential-backoff: "npm:^3.1.1"
@@ -2506,7 +2507,7 @@ __metadata:
which: "npm:^5.0.0"
bin:
node-gyp: bin/node-gyp.js
checksum: 10c0/bd8d8c76b06be761239b0c8680f655f6a6e90b48e44d43415b11c16f7e8c15be346fba0cbf71588c7cdfb52c419d928a7d3db353afc1d952d19756237d8f10b9
checksum: 10c0/5f4ad5a729386f7b50096efd4934b06c071dbfbc7d7d541a66d6959a7dccd62f53ff3dc95fffb60bf99d8da1270e23769f82246fcaa6c5645a70c967ae9a3398
languageName: node
linkType: hard
@@ -3137,6 +3138,26 @@ __metadata:
languageName: node
linkType: hard
"typescript@npm:^5.9.2":
version: 5.9.2
resolution: "typescript@npm:5.9.2"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: 10c0/cd635d50f02d6cf98ed42de2f76289701c1ec587a363369255f01ed15aaf22be0813226bff3c53e99d971f9b540e0b3cc7583dbe05faded49b1b0bed2f638a18
languageName: node
linkType: hard
"typescript@patch:typescript@npm%3A^5.9.2#optional!builtin<compat/typescript>":
version: 5.9.2
resolution: "typescript@patch:typescript@npm%3A5.9.2#optional!builtin<compat/typescript>::version=5.9.2&hash=5786d5"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: 10c0/34d2a8e23eb8e0d1875072064d5e1d9c102e0bdce56a10a25c0b917b8aa9001a9cf5c225df12497e99da107dc379360bc138163c66b55b95f5b105b50578067e
languageName: node
linkType: hard
"unicorn-magic@npm:^0.1.0":
version: 0.1.0
resolution: "unicorn-magic@npm:0.1.0"