Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,27 @@ initializeSQLite();
The `db` object above implements the
[Object-Oriented API #1](https://sqlite.org/wasm/doc/trunk/api-oo1.md).

### Importing only selected optional support:

The default `@sqlite.org/sqlite-wasm` entry keeps backwards compatibility and includes all bundled
VFS support, virtual table helpers, and Worker API #1. To let bundlers include only the optional
code you need, import the core runtime and one or more optional modules before calling
`sqlite3InitModule()`:

```js
import '@sqlite.org/sqlite-wasm/vfs/kvvfs';
import sqlite3InitModule from '@sqlite.org/sqlite-wasm/core';

const sqlite3 = await sqlite3InitModule();
```

Available optional modules are `@sqlite.org/sqlite-wasm/vfs/kvvfs`,
`@sqlite.org/sqlite-wasm/vfs/opfs`, `@sqlite.org/sqlite-wasm/vfs/opfs-sahpool`,
`@sqlite.org/sqlite-wasm/vfs/opfs-wl`, and `@sqlite.org/sqlite-wasm/vtab`.

The core entry intentionally does not include Worker API #1. Use the default
`@sqlite.org/sqlite-wasm` entry if you need `sqlite3.initWorker1API()` or `sqlite3Worker1Promiser`.

## Usage with vite

If you are using [vite](https://vitejs.dev/), you need to add the following config option in
Expand Down
39 changes: 39 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@
"opfs",
"origin-private-file-system"
],
"sideEffects": [
"./dist/index.mjs",
"./dist/vtab.mjs",
"./dist/vfs/*.mjs",
"./dist/sqlite3-vfs-*.mjs",
"./dist/sqlite3-worker1-api-*.mjs"
],
"publishConfig": {
"access": "public"
},
Expand All @@ -38,6 +45,36 @@
"main": "./dist/index.mjs",
"browser": "./dist/index.mjs"
},
"./core": {
"types": "./dist/core.d.mts",
"import": "./dist/core.mjs",
"browser": "./dist/core.mjs"
},
"./vtab": {
"types": "./dist/vtab.d.mts",
"import": "./dist/vtab.mjs",
"browser": "./dist/vtab.mjs"
},
"./vfs/kvvfs": {
"types": "./dist/vfs/kvvfs.d.mts",
"import": "./dist/vfs/kvvfs.mjs",
"browser": "./dist/vfs/kvvfs.mjs"
},
"./vfs/opfs": {
"types": "./dist/vfs/opfs.d.mts",
"import": "./dist/vfs/opfs.mjs",
"browser": "./dist/vfs/opfs.mjs"
},
"./vfs/opfs-sahpool": {
"types": "./dist/vfs/opfs-sahpool.d.mts",
"import": "./dist/vfs/opfs-sahpool.mjs",
"browser": "./dist/vfs/opfs-sahpool.mjs"
},
"./vfs/opfs-wl": {
"types": "./dist/vfs/opfs-wl.d.mts",
"import": "./dist/vfs/opfs-wl.mjs",
"browser": "./dist/vfs/opfs-wl.mjs"
},
"./package.json": "./package.json",
"./sqlite3.wasm": "./dist/sqlite3.wasm"
},
Expand All @@ -49,6 +86,8 @@
"README.md"
],
"scripts": {
"split:vfs": "node scripts/split-bundler-friendly-vfss.mjs",
"prebuild": "npm run split:vfs",
"test": "vitest",
"test:node": "vitest --project node",
"test:browser": "vitest --project browser",
Expand Down
142 changes: 142 additions & 0 deletions scripts/split-bundler-friendly-vfss.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const sourcePath = path.join(rootDir, 'src/bin/sqlite3-bundler-friendly.mjs');
const outputDir = path.join(rootDir, 'src/bin');

const generatedHeader = (
sourceFile,
) => `// Generated by scripts/split-bundler-friendly-vfss.mjs from ${sourceFile}.
// Do not edit this file directly.
`;

const initRegistry = `const sqlite3BundlerFriendlyOptionalState =
(globalThis.__sqlite3BundlerFriendlyOptional ??= {
initializers: Object.create(null),
});
const sqlite3BundlerFriendlyRegisterOptionalInitializer = (slot, initializer) => {
if (globalThis.sqlite3ApiBootstrap?.initializers) {
globalThis.sqlite3ApiBootstrap.initializers.push(initializer);
} else {
(sqlite3BundlerFriendlyOptionalState.initializers[slot] ??= []).push(initializer);
}
};
`;

const baseHook = `const sqlite3BundlerFriendlyInstallOptionalInitializers = (slot) => {
const sqlite3BundlerFriendlyOptionalState = globalThis.__sqlite3BundlerFriendlyOptional;
const initializers = sqlite3BundlerFriendlyOptionalState?.initializers?.[slot];
if (initializers?.length) {
globalThis.sqlite3ApiBootstrap.initializers.push(
...initializers,
);
}
};
`;

const markerSpecs = {
worker1: 'This file implements the initializer for SQLite\'s "Worker API #1"',
helper: 'This file installs sqlite3.vfs, a namespace of helpers',
vtab: 'This file installs sqlite3.vtab, a namespace of helpers',
kvvfs: 'This file houses the "kvvfs" pieces of the SQLite3 JS API',
opfsShared: 'This file holds code shared by sqlite3-vfs-opfs{,-wl}.c-pp.js',
opfs: 'This file holds the synchronous half of an sqlite3_vfs',
sahpool: 'This file holds a sqlite3_vfs backed by OPFS storage',
opfsWl: 'This file is a reimplementation of the "opfs" VFS',
tail: 'This file is the tail end of the sqlite3-api.js constellation',
};

const findCommentStart = (source, marker) => {
const markerIndex = source.indexOf(marker);
if (markerIndex < 0) {
throw new Error(`Unable to find marker: ${marker}`);
}
const commentStart = source.lastIndexOf('/*', markerIndex);
if (commentStart < 0) {
throw new Error(`Unable to find comment start for marker: ${marker}`);
}
return commentStart;
};

const replaceAll = (value, replacements) => {
let result = value;
for (const [from, to] of replacements) {
result = result.split(from).join(to);
}
return result;
};

const source = await readFile(sourcePath, 'utf8');
const markers = Object.fromEntries(
Object.entries(markerSpecs).map(([name, marker]) => [name, findCommentStart(source, marker)]),
);

const ranges = {
worker1: [markers.worker1, markers.helper],
helper: [markers.helper, markers.vtab],
vtab: [markers.vtab, markers.kvvfs],
kvvfs: [markers.kvvfs, markers.opfsShared],
opfsShared: [markers.opfsShared, markers.opfs],
opfs: [markers.opfs, markers.sahpool],
sahpool: [markers.sahpool, markers.opfsWl],
opfsWl: [markers.opfsWl, markers.tail],
};

for (const [name, [start, end]] of Object.entries(ranges)) {
if (start >= end) {
throw new Error(`Invalid ${name} range: ${start} >= ${end}`);
}
}

const bootstrapAsyncMarker = 'globalThis.sqlite3ApiBootstrap.initializersAsync = [];\n';
if (!source.includes(bootstrapAsyncMarker)) {
throw new Error(`Unable to find bootstrap hook marker: ${bootstrapAsyncMarker.trim()}`);
}

const strippedSource = Object.entries(ranges)
.sort((a, b) => b[1][0] - a[1][0])
.reduce(
(current, [name, [start, end]]) =>
current.slice(0, start) +
`\n/* Optional initializer slot removed by scripts/split-bundler-friendly-vfss.mjs: ${name} */\n` +
`sqlite3BundlerFriendlyInstallOptionalInitializers('${name}');\n` +
current.slice(end),
source,
)
.replace(bootstrapAsyncMarker, bootstrapAsyncMarker + baseHook);

const outputFileNames = {
worker1: 'sqlite3-worker1-api.mjs',
};

const toOptionalModule = (slot, chunk) =>
generatedHeader('sqlite3-bundler-friendly.mjs') +
initRegistry +
replaceAll(chunk, [
[
'globalThis.sqlite3ApiBootstrap.initializers.push(',
`sqlite3BundlerFriendlyRegisterOptionalInitializer('${slot}', `,
],
]);

await mkdir(outputDir, { recursive: true });

await writeFile(
path.join(outputDir, 'sqlite3-bundler-friendly.core.mjs'),
generatedHeader('sqlite3-bundler-friendly.mjs') + strippedSource,
);

await Promise.all(
Object.entries(ranges).map(([name, [start, end]]) =>
writeFile(
path.join(outputDir, outputFileNames[name] ?? `sqlite3-vfs-${name}.mjs`),
toOptionalModule(name, source.slice(start, end)),
),
),
);
3 changes: 3 additions & 0 deletions src/__tests__/sqlite3-oo1.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import type { SqlValue } from '../index';
test('Bundler-friendly OO1 API sanity check (browser)', async () => {
const sqlite3 = await sqlite3InitModule();

expect(sqlite3.initWorker1API).toBeTypeOf('function');
expect(sqlite3.vtab).toBeDefined();

// 1. Create a database
const db = new sqlite3.oo1.DB(':memory:');
expect(db.isOpen()).toBe(true);
Expand Down
14 changes: 10 additions & 4 deletions src/__tests__/sqlite3-opfs.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,17 @@ const runWorker = async (workerUrl: URL): Promise<void> => {
};

describe('opfs persistence APIs', () => {
test('OpfsDb sanity check in Worker (browser)', async () => {
await runWorker(new URL('./workers/sqlite3-opfs.worker.ts', import.meta.url));
test.each([
['default entry', './workers/sqlite3-opfs.worker.ts'],
['treeshakable entry', './workers/sqlite3-opfs-treeshakable.worker.ts'],
])('OpfsDb sanity check in Worker (browser, %s)', async (_label, workerPath) => {
await runWorker(new URL(workerPath, import.meta.url));
});

test('OpfsWlDb sanity check in Worker (browser)', async () => {
await runWorker(new URL('./workers/sqlite3-opfs-wl.worker.ts', import.meta.url));
test.each([
['default entry', './workers/sqlite3-opfs-wl.worker.ts'],
['treeshakable entry', './workers/sqlite3-opfs-wl-treeshakable.worker.ts'],
])('OpfsWlDb sanity check in Worker (browser, %s)', async (_label, workerPath) => {
await runWorker(new URL(workerPath, import.meta.url));
});
});
9 changes: 5 additions & 4 deletions src/__tests__/sqlite3-sahpool-vfs.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ type WorkerSuccessMessage = {
type: 'success';
};

test('OpfsSAHPoolVfs sanity check in Worker (browser)', async () => {
const worker = new Worker(new URL('./workers/sqlite3-sahpool.worker.ts', import.meta.url), {
type: 'module',
});
test.each([
['default entry', './workers/sqlite3-sahpool.worker.ts'],
['treeshakable entry', './workers/sqlite3-sahpool-treeshakable.worker.ts'],
])('OpfsSAHPoolVfs sanity check in Worker (browser, %s)', async (_label, workerPath) => {
const worker = new Worker(new URL(workerPath, import.meta.url), { type: 'module' });

try {
const result = await new Promise<WorkerSuccessMessage>((resolve, reject) => {
Expand Down
41 changes: 41 additions & 0 deletions src/__tests__/sqlite3-split-vfs.browser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { expect, test } from 'vitest';

type WorkerMessage = {
type: 'success';
};

const runWorker = async (workerUrl: URL): Promise<void> => {
const worker = new Worker(workerUrl, { type: 'module' });

try {
const result = await new Promise<WorkerMessage>((resolve, reject) => {
worker.onmessage = (e) => {
if (e.data.type === 'success') {
resolve(e.data);
} else {
reject(new Error(e.data.message || 'Unknown worker error'));
}
};
worker.onerror = (e) => {
reject(new Error('Worker error: ' + e.message));
};
worker.postMessage({ type: 'start' });
});

expect(result.type).toBe('success');
} finally {
worker.terminate();
}
};

test('core runtime initializes without optional VFS modules', async () => {
await runWorker(new URL('./workers/sqlite3-core.worker.ts', import.meta.url));
});

test('core runtime can opt into only kvvfs', async () => {
await runWorker(new URL('./workers/sqlite3-core-kvvfs.worker.ts', import.meta.url));
});

test('core runtime can opt into only vtab helpers', async () => {
await runWorker(new URL('./workers/sqlite3-core-vtab.worker.ts', import.meta.url));
});
Loading