From 326f33dba26a05d5024e07ae25d7239c3f2f7ede Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Fri, 3 Oct 2025 10:15:08 +0200 Subject: [PATCH] bundle powersync core with package --- .changeset/seven-fireants-boil.md | 5 ++ packages/node/download_core.js | 113 +++++++++++++++++++-------- packages/node/package.json | 8 +- packages/node/src/db/SqliteWorker.ts | 28 +++++-- 4 files changed, 113 insertions(+), 41 deletions(-) create mode 100644 .changeset/seven-fireants-boil.md diff --git a/.changeset/seven-fireants-boil.md b/.changeset/seven-fireants-boil.md new file mode 100644 index 000000000..28f9e5ccf --- /dev/null +++ b/.changeset/seven-fireants-boil.md @@ -0,0 +1,5 @@ +--- +'@powersync/node': minor +--- + +Bundle PowerSync extension with NPM package diff --git a/packages/node/download_core.js b/packages/node/download_core.js index 703295df2..ecbb07b1d 100644 --- a/packages/node/download_core.js +++ b/packages/node/download_core.js @@ -1,11 +1,8 @@ -// TODO: Make this a pre-publish hook and just bundle everything import { createHash } from 'node:crypto'; -import * as OS from 'node:os'; import * as fs from 'node:fs/promises'; import * as path from 'node:path'; import { Readable } from 'node:stream'; import { finished } from 'node:stream/promises'; -import { exit } from 'node:process'; // When changing this version, run node download_core.js update_hashes const version = '0.4.6'; @@ -17,23 +14,14 @@ const versionHashes = { 'libpowersync_aarch64.dylib': 'bfb4f1ec207b298aff560f1825f8123d24316edaa27b6df3a17dd49466576b92' }; -const platform = OS.platform(); -let destination; -let asset; - -if (platform === 'win32') { - asset = 'powersync_x64.dll'; - destination = 'powersync.dll'; -} else if (platform === 'linux') { - asset = OS.arch() === 'x64' ? 'libpowersync_x64.so' : 'libpowersync_aarch64.so'; - destination = 'libpowersync.so'; -} else if (platform === 'darwin') { - asset = OS.arch() === 'x64' ? 'libpowersync_x64.dylib' : 'libpowersync_aarch64.dylib'; - destination = 'libpowersync.dylib'; -} - -const expectedHash = versionHashes[asset]; -const destinationPath = path.resolve('lib', destination); +// Map of all assets to their destinations +const assetMap = { + 'powersync_x64.dll': 'powersync.dll', + 'libpowersync_x64.so': 'libpowersync.so', + 'libpowersync_aarch64.so': 'libpowersync-aarch64.so', + 'libpowersync_x64.dylib': 'libpowersync.dylib', + 'libpowersync_aarch64.dylib': 'libpowersync-aarch64.dylib' +}; const hashStream = async (input) => { for await (const chunk of input.pipe(createHash('sha256')).setEncoding('hex')) { @@ -41,9 +29,9 @@ const hashStream = async (input) => { } }; -const hashLocal = async () => { +const hashLocal = async (filePath) => { try { - const handle = await fs.open(destinationPath, 'r'); + const handle = await fs.open(filePath, 'r'); const input = handle.createReadStream(); const result = await hashStream(input); @@ -54,31 +42,92 @@ const hashLocal = async () => { } }; -const download = async () => { - if ((await hashLocal()) == expectedHash) { - console.debug('Local copy is up-to-date, skipping download'); - exit(0); +const downloadAsset = async (asset, destination) => { + const destinationPath = path.resolve('lib', destination); + const expectedHash = versionHashes[asset]; + + // Check if file exists and has correct hash + const currentHash = await hashLocal(destinationPath); + if (currentHash === expectedHash) { + console.debug(`${destination} is up-to-date, skipping download`); + return; } const url = `https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v${version}/${asset}`; + console.log(`Downloading ${url}`); const response = await fetch(url); if (response.status != 200) { throw `Could not download ${url}`; } + const file = await fs.open(destinationPath, 'w'); + await finished(Readable.fromWeb(response.body).pipe(file.createWriteStream())); + await file.close(); + + const hashAfterDownloading = await hashLocal(destinationPath); + if (hashAfterDownloading != expectedHash) { + throw `Unexpected hash after downloading ${asset} (got ${hashAfterDownloading}, expected ${expectedHash})`; + } + console.log(`Successfully downloaded ${destination}`); +}; + +const checkAsset = async (asset, destination) => { + const destinationPath = path.resolve('lib', destination); + const expectedHash = versionHashes[asset]; + const currentHash = await hashLocal(destinationPath); + + return { + asset, + destination, + destinationPath, + expectedHash, + currentHash, + exists: currentHash !== null, + isValid: currentHash === expectedHash + }; +}; + +const download = async () => { try { await fs.access('lib'); } catch { await fs.mkdir('lib'); } - const file = await fs.open(destinationPath, 'w'); - await finished(Readable.fromWeb(response.body).pipe(file.createWriteStream())); - await file.close(); + // First check all assets + console.log('Checking existing files...'); + const checks = await Promise.all( + Object.entries(assetMap).map(([asset, destination]) => checkAsset(asset, destination)) + ); - const hashAfterDownloading = await hashLocal(); - if (hashAfterDownloading != expectedHash) { - throw `Unexpected hash after downloading (got ${hashAfterDownloading}, expected ${expectedHash})`; + const toDownload = checks.filter((check) => !check.isValid); + const upToDate = checks.filter((check) => check.isValid); + + // Print summary + if (upToDate.length > 0) { + console.log('\nUp-to-date files:'); + for (const check of upToDate) { + console.log(` ✓ ${check.destination}`); + } + } + + if (toDownload.length > 0) { + console.log('\nFiles to download:'); + for (const check of toDownload) { + if (!check.exists) { + console.log(` • ${check.destination} (missing)`); + } else { + console.log(` • ${check.destination} (hash mismatch)`); + } + } + + console.log('\nStarting downloads...'); + // Download required assets in parallel + await Promise.all(toDownload.map((check) => downloadAsset(check.asset, check.destination))); + + console.log('\nAll downloads completed successfully!'); + } else { + console.log('\nAll files are up-to-date, nothing to download.'); } }; diff --git a/packages/node/package.json b/packages/node/package.json index e87ede1b7..c0e0a2b36 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -13,12 +13,12 @@ "download_core.js" ], "scripts": { - "install": "node download_core.js", - "build": "tsc -b && rollup --config", - "build:prod": "tsc -b --sourceMap false && rollup --config", + "prepare:core": "node download_core.js", + "build": " pnpm prepare:core && tsc -b && rollup --config", + "build:prod": "pnpm prepare:core && tsc -b --sourceMap false && rollup --config", "clean": "rm -rf lib dist tsconfig.tsbuildinfo", "watch": "tsc -b -w", - "test": "vitest", + "test": " pnpm prepare:core && vitest", "test:exports": "attw --pack . --ignore-rules no-resolution" }, "type": "module", diff --git a/packages/node/src/db/SqliteWorker.ts b/packages/node/src/db/SqliteWorker.ts index 2bb3f8e3e..fd4ffe079 100644 --- a/packages/node/src/db/SqliteWorker.ts +++ b/packages/node/src/db/SqliteWorker.ts @@ -1,9 +1,9 @@ -import * as path from 'node:path'; import BetterSQLite3Database, { Database } from '@powersync/better-sqlite3'; import * as Comlink from 'comlink'; -import { parentPort, threadId } from 'node:worker_threads'; import OS from 'node:os'; +import * as path from 'node:path'; import url from 'node:url'; +import { parentPort, threadId } from 'node:worker_threads'; import { AsyncDatabase, AsyncDatabaseOpener } from './AsyncDatabase.js'; class BlockingAsyncDatabase implements AsyncDatabase { @@ -129,13 +129,31 @@ export function startPowerSyncWorker(options?: Partial) const isCommonJsModule = import.meta.isBundlingToCommonJs ?? false; const platform = OS.platform(); + const arch = OS.arch(); let extensionPath: string; + if (platform === 'win32') { - extensionPath = 'powersync.dll'; + if (arch === 'x64') { + extensionPath = 'powersync.dll'; + } else { + throw 'Windows platform only supports x64 architecture.'; + } } else if (platform === 'linux') { - extensionPath = 'libpowersync.so'; + if (arch === 'x64') { + extensionPath = 'libpowersync.so'; + } else if (arch === 'arm64') { + extensionPath = 'libpowersync-aarch64.so'; + } else { + throw 'Linux platform only supports x64 and arm64 architectures.'; + } } else if (platform === 'darwin') { - extensionPath = 'libpowersync.dylib'; + if (arch === 'x64') { + extensionPath = 'libpowersync.dylib'; + } else if (arch === 'arm64') { + extensionPath = 'libpowersync-aarch64.dylib'; + } else { + throw 'macOS platform only supports x64 and arm64 architectures.'; + } } else { throw 'Unknown platform, PowerSync for Node.js currently supports Windows, Linux and macOS.'; }