Skip to content

Commit 73f07cc

Browse files
refactor: replace better-sqlite3 with node:sqlite
- Use Node.js native node:sqlite module (available from Node 22.5.0) - Add Node.js version check at startup - Add engines field to package.json - Use named parameters consistently in SQL queries - Suppress ExperimentalWarning for node:sqlite in shebang and CI Benefits: - Zero native compilation dependencies (no node-gyp) - Built-in Node.js module - will stabilize over time - Same performance - local file-based SQLite database
1 parent 1c125c9 commit 73f07cc

9 files changed

Lines changed: 135 additions & 29 deletions

File tree

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ jobs:
8282
run: node ./bin/bundle.js bundle -p docs-search-client-halogen
8383

8484
- name: Run tests
85-
run: node ./bin/bundle.js test
85+
run: node --no-warnings=ExperimentalWarning ./bin/bundle.js test
8686

8787
- name: Check formatting (Linux only)
8888
if: matrix.os == 'ubuntu-latest'

bin/index.dev.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#!/usr/bin/env node
1+
#!/usr/bin/env -S node --no-warnings=ExperimentalWarning
22

33
import { main } from "../output/Main/index.js";
44

bin/src/Main.purs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import Data.Maybe as Maybe
1414
import Data.Set as Set
1515
import Effect.Aff as Aff
1616
import Effect.Aff.AVar as AVar
17+
import Effect.Console as Console
1718
import Effect.Now as Now
19+
import Node.Process as Process
1820
import Options.Applicative (CommandFields, Mod, Parser, ParserPrefs(..))
1921
import Options.Applicative as O
2022
import Options.Applicative.Types (Backtracking(..))
@@ -51,6 +53,8 @@ import Spago.Generated.BuildInfo as BuildInfo
5153
import Spago.Git as Git
5254
import Spago.Json as Json
5355
import Spago.Log (LogVerbosity(..))
56+
import Spago.NodeVersion (NodeVersionCheck(..))
57+
import Spago.NodeVersion as NodeVersion
5458
import Spago.Path as Path
5559
import Spago.Paths as Paths
5660
import Spago.Purs as Purs
@@ -531,6 +535,7 @@ parseArgs = do
531535

532536
main :: Effect Unit
533537
main = do
538+
ensureMinimumNodeVersion
534539
startingTime <- Now.now
535540
parseArgs >>=
536541
\c -> Aff.launchAff_ case c of
@@ -1049,3 +1054,14 @@ mkDocsEnv args dependencies = do
10491054
}
10501055

10511056
foreign import supportsColor :: Effect Boolean
1057+
1058+
-- | Ensures Node.js version is >= 22.5.0 (required for node:sqlite)
1059+
ensureMinimumNodeVersion :: Effect Unit
1060+
ensureMinimumNodeVersion =
1061+
case NodeVersion.checkNodeVersion { major: 22, minor: 5 } Process.version of
1062+
NodeVersionOk -> pure unit
1063+
NodeVersionTooOld v -> do
1064+
Console.error $ "Error: spago requires Node.js v22.5.0 or later (found " <> v <> ")"
1065+
Process.exit' 1
1066+
NodeVersionUnparseable v ->
1067+
Console.warn $ "Warning: spago could not parse the Node.js version: " <> v

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
},
1818
"author": "Fabrizio Ferrai",
1919
"type": "module",
20+
"engines": {
21+
"node": ">=22.5.0"
22+
},
2023
"bin": {
2124
"spago": "bin/bundle.js"
2225
},
@@ -35,7 +38,6 @@
3538
},
3639
"dependencies": {
3740
"@nodelib/fs.walk": "^3.0.1",
38-
"better-sqlite3": "^12.5.0",
3941
"env-paths": "^3.0.0",
4042
"fs-extra": "^11.3.0",
4143
"fuse.js": "^7.1.0",

src/Spago/Db.js

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1-
import Database from "better-sqlite3";
1+
import { DatabaseSync } from "node:sqlite";
2+
import fs from "node:fs";
3+
import path from "node:path";
24

3-
export const connectImpl = (path, logger) => {
4-
logger("Connecting to database at " + path);
5-
let db = new Database(path, {
6-
fileMustExist: false,
7-
// verbose: logger,
5+
export const connectImpl = (databasePath, logger) => {
6+
logger("Connecting to database at " + databasePath);
7+
8+
// Ensure directory exists
9+
const dir = path.dirname(databasePath);
10+
fs.mkdirSync(dir, { recursive: true });
11+
12+
const db = new DatabaseSync(databasePath, {
13+
enableForeignKeyConstraints: true,
814
});
9-
db.pragma("journal_mode = WAL");
10-
db.pragma("foreign_keys = ON");
15+
16+
db.exec("PRAGMA journal_mode = WAL");
17+
db.exec("PRAGMA foreign_keys = ON");
1118

1219
db.prepare(`CREATE TABLE IF NOT EXISTS package_sets
1320
( version TEXT PRIMARY KEY NOT NULL
@@ -31,21 +38,21 @@ export const connectImpl = (path, logger) => {
3138
, last_fetched TEXT NOT NULL
3239
)`).run();
3340
// it would be lovely if we'd have a foreign key on package_metadata, but that would
34-
// require reading metadatas before manifests, which we can't always guarantee
41+
// require reading metadata before manifests, which we can't always guarantee
3542
db.prepare(`CREATE TABLE IF NOT EXISTS package_manifests
3643
( name TEXT NOT NULL
3744
, version TEXT NOT NULL
3845
, manifest TEXT NOT NULL
3946
, PRIMARY KEY (name, version)
4047
)`).run();
4148
return db;
42-
};
49+
}
4350

4451
export const insertPackageSetImpl = (db, packageSet) => {
4552
db.prepare(
4653
"INSERT INTO package_sets (version, compiler, date) VALUES (@version, @compiler, @date)"
4754
).run(packageSet);
48-
};
55+
}
4956

5057
export const insertPackageSetEntryImpl = (db, packageSetEntry) => {
5158
db.prepare(
@@ -55,8 +62,8 @@ export const insertPackageSetEntryImpl = (db, packageSetEntry) => {
5562

5663
export const selectLatestPackageSetByCompilerImpl = (db, compiler) => {
5764
const row = db
58-
.prepare("SELECT * FROM package_sets WHERE compiler = ? ORDER BY date DESC LIMIT 1")
59-
.get(compiler);
65+
.prepare("SELECT * FROM package_sets WHERE compiler = @compiler ORDER BY date DESC LIMIT 1")
66+
.get({ compiler });
6067
return row;
6168
}
6269

@@ -69,22 +76,22 @@ export const selectPackageSetsImpl = (db) => {
6976

7077
export const selectPackageSetEntriesBySetImpl = (db, packageSetVersion) => {
7178
const row = db
72-
.prepare("SELECT * FROM package_set_entries WHERE packageSetVersion = ?")
73-
.all(packageSetVersion);
79+
.prepare("SELECT * FROM package_set_entries WHERE packageSetVersion = @packageSetVersion")
80+
.all({ packageSetVersion });
7481
return row;
7582
}
7683

7784
export const selectPackageSetEntriesByPackageImpl = (db, packageName, packageVersion) => {
7885
const row = db
79-
.prepare("SELECT * FROM package_set_entries WHERE packageName = ? AND packageVersion = ?")
80-
.all(packageName, packageVersion);
86+
.prepare("SELECT * FROM package_set_entries WHERE packageName = @packageName AND packageVersion = @packageVersion")
87+
.all({ packageName, packageVersion });
8188
return row;
8289
}
8390

8491
export const getLastPullImpl = (db, key) => {
8592
const row = db
86-
.prepare("SELECT * FROM last_git_pull WHERE key = ? LIMIT 1")
87-
.get(key);
93+
.prepare("SELECT * FROM last_git_pull WHERE key = @key LIMIT 1")
94+
.get({ key });
8895
return row?.date;
8996
}
9097

@@ -94,8 +101,8 @@ export const updateLastPullImpl = (db, key, date) => {
94101

95102
export const getManifestImpl = (db, name, version) => {
96103
const row = db
97-
.prepare("SELECT * FROM package_manifests WHERE name = ? AND version = ? LIMIT 1")
98-
.get(name, version);
104+
.prepare("SELECT * FROM package_manifests WHERE name = @name AND version = @version LIMIT 1")
105+
.get({ name, version });
99106
return row?.manifest;
100107
}
101108

@@ -104,7 +111,7 @@ export const insertManifestImpl = (db, name, version, manifest) => {
104111
}
105112

106113
export const removeManifestImpl = (db, name, version) => {
107-
db.prepare("DELETE FROM package_manifests WHERE name = ? AND version = ?").run(name, version);
114+
db.prepare("DELETE FROM package_manifests WHERE name = @name AND version = @version").run({ name, version });
108115
}
109116

110117
export const insertMetadataImpl = (db, name, metadata, last_fetched) => {
@@ -113,6 +120,6 @@ export const insertMetadataImpl = (db, name, metadata, last_fetched) => {
113120

114121
export const getMetadataForPackagesImpl = (db, names) => {
115122
// There can be a lot of package names here, potentially hitting the max number of sqlite parameters, so we use json to bypass this
116-
const query = db.prepare("SELECT * FROM package_metadata WHERE name IN (SELECT value FROM json_each(?));");
117-
return query.all(JSON.stringify(names));
118-
};
123+
const query = db.prepare("SELECT * FROM package_metadata WHERE name IN (SELECT value FROM json_each(@names));");
124+
return query.all({ names: JSON.stringify(names) });
125+
}

src/Spago/NodeVersion.purs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
module Spago.NodeVersion
2+
( NodeVersionCheck(..)
3+
, checkNodeVersion
4+
) where
5+
6+
import Prelude
7+
8+
import Data.Array as Array
9+
import Data.Int as Int
10+
import Data.Maybe (Maybe(..), fromMaybe)
11+
import Data.String as String
12+
import Data.Traversable (traverse)
13+
14+
data NodeVersionCheck
15+
= NodeVersionOk
16+
| NodeVersionTooOld String
17+
| NodeVersionUnparseable String
18+
19+
derive instance Eq NodeVersionCheck
20+
instance Show NodeVersionCheck where
21+
show NodeVersionOk = "NodeVersionOk"
22+
show (NodeVersionTooOld v) = "(NodeVersionTooOld " <> show v <> ")"
23+
show (NodeVersionUnparseable v) = "(NodeVersionUnparseable " <> show v <> ")"
24+
25+
-- | Check if a version string meets the minimum Node.js version requirement
26+
checkNodeVersion :: { major :: Int, minor :: Int } -> String -> NodeVersionCheck
27+
checkNodeVersion minimum version =
28+
let
29+
-- version is like "v22.5.0" or "22.5.0"
30+
versionStr = String.stripPrefix (String.Pattern "v") version
31+
# fromMaybe version
32+
parts = String.split (String.Pattern ".") versionStr
33+
in
34+
case traverse Int.fromString (Array.take 2 parts) of
35+
Just [major, minor]
36+
| major > minimum.major -> NodeVersionOk
37+
| major == minimum.major && minor >= minimum.minor -> NodeVersionOk
38+
| otherwise -> NodeVersionTooOld version
39+
_ -> NodeVersionUnparseable version

test/Prelude.purs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ withTempDir = Aff.bracket createTempDir cleanupTempDir
6565
spago' stdin args =
6666
Cmd.exec
6767
(Path.global "node")
68-
([ Path.toRaw $ oldCwd </> "bin" </> "index.dev.js" ] <> args)
68+
([ "--no-warnings=ExperimentalWarning", Path.toRaw $ oldCwd </> "bin" </> "index.dev.js" ] <> args)
6969
$ Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false, pipeStdin = stdin }
7070

7171
spago = spago' StdinNewPipe

test/Spago/Unit.purs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Prelude
55
import Test.Spago.Unit.CheckInjectivity as CheckInjectivity
66
import Test.Spago.Unit.FindFlags as FindFlags
77
import Test.Spago.Unit.Git as Git
8+
import Test.Spago.Unit.NodeVersion as NodeVersion
89
import Test.Spago.Unit.Path as Path
910
import Test.Spago.Unit.Printer as Printer
1011
import Test.Spec (Spec)
@@ -17,3 +18,4 @@ spec = Spec.describe "unit" do
1718
Printer.spec
1819
Git.spec
1920
Path.spec
21+
NodeVersion.spec

test/Spago/Unit/NodeVersion.purs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
module Test.Spago.Unit.NodeVersion where
2+
3+
import Prelude
4+
5+
import Spago.NodeVersion (NodeVersionCheck(..), checkNodeVersion)
6+
import Test.Spec (Spec)
7+
import Test.Spec as Spec
8+
import Test.Spec.Assertions as Assertions
9+
10+
minimum :: { major :: Int, minor :: Int }
11+
minimum = { major: 22, minor: 5 }
12+
13+
spec :: Spec Unit
14+
spec = do
15+
Spec.describe "checkNodeVersion" do
16+
Spec.describe "accepts valid versions" do
17+
Spec.it "v22.5.0" do
18+
checkNodeVersion minimum "v22.5.0" `Assertions.shouldEqual` NodeVersionOk
19+
Spec.it "22.5.0" do
20+
checkNodeVersion minimum "22.5.0" `Assertions.shouldEqual` NodeVersionOk
21+
Spec.it "v22.6.0" do
22+
checkNodeVersion minimum "v22.6.0" `Assertions.shouldEqual` NodeVersionOk
23+
Spec.it "v23.0.0" do
24+
checkNodeVersion minimum "v23.0.0" `Assertions.shouldEqual` NodeVersionOk
25+
Spec.it "v25.2.1" do
26+
checkNodeVersion minimum "v25.2.1" `Assertions.shouldEqual` NodeVersionOk
27+
28+
Spec.describe "rejects old versions" do
29+
Spec.it "v22.4.0" do
30+
checkNodeVersion minimum "v22.4.0" `Assertions.shouldEqual` NodeVersionTooOld "v22.4.0"
31+
Spec.it "v21.0.0" do
32+
checkNodeVersion minimum "v21.0.0" `Assertions.shouldEqual` NodeVersionTooOld "v21.0.0"
33+
Spec.it "v18.17.0" do
34+
checkNodeVersion minimum "v18.17.0" `Assertions.shouldEqual` NodeVersionTooOld "v18.17.0"
35+
36+
Spec.describe "handles unparseable versions" do
37+
Spec.it "garbage" do
38+
checkNodeVersion minimum "garbage" `Assertions.shouldEqual` NodeVersionUnparseable "garbage"
39+
Spec.it "empty string" do
40+
checkNodeVersion minimum "" `Assertions.shouldEqual` NodeVersionUnparseable ""

0 commit comments

Comments
 (0)