Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ jobs:
run: node ./bin/bundle.js bundle -p docs-search-client-halogen

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

- name: Check formatting (Linux only)
if: matrix.os == 'ubuntu-latest'
Expand Down
2 changes: 1 addition & 1 deletion bin/index.dev.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env node
#!/usr/bin/env -S node --no-warnings=ExperimentalWarning

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

Expand Down
16 changes: 16 additions & 0 deletions bin/src/Main.purs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import Data.Maybe as Maybe
import Data.Set as Set
import Effect.Aff as Aff
import Effect.Aff.AVar as AVar
import Effect.Console as Console
import Effect.Now as Now
import Node.Process as Process
import Options.Applicative (CommandFields, Mod, Parser, ParserPrefs(..))
import Options.Applicative as O
import Options.Applicative.Types (Backtracking(..))
Expand Down Expand Up @@ -51,6 +53,8 @@ import Spago.Generated.BuildInfo as BuildInfo
import Spago.Git as Git
import Spago.Json as Json
import Spago.Log (LogVerbosity(..))
import Spago.NodeVersion (NodeVersionCheck(..))
import Spago.NodeVersion as NodeVersion
import Spago.Path as Path
import Spago.Paths as Paths
import Spago.Purs as Purs
Expand Down Expand Up @@ -531,6 +535,7 @@ parseArgs = do

main :: Effect Unit
main = do
ensureMinimumNodeVersion
startingTime <- Now.now
parseArgs >>=
\c -> Aff.launchAff_ case c of
Expand Down Expand Up @@ -1049,3 +1054,14 @@ mkDocsEnv args dependencies = do
}

foreign import supportsColor :: Effect Boolean

-- | Ensures Node.js version is >= 22.5.0 (required for node:sqlite)
ensureMinimumNodeVersion :: Effect Unit
ensureMinimumNodeVersion =
case NodeVersion.checkNodeVersion { major: 22, minor: 5 } Process.version of
NodeVersionOk -> pure unit
NodeVersionTooOld v -> do
Console.error $ "Error: spago requires Node.js v22.5.0 or later (found " <> v <> ")"
Process.exit' 1
NodeVersionUnparseable v ->
Console.warn $ "Warning: spago could not parse the Node.js version: " <> v
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
},
"author": "Fabrizio Ferrai",
"type": "module",
"engines": {
"node": ">=22.5.0"
},
"bin": {
"spago": "bin/bundle.js"
},
Expand All @@ -35,7 +38,6 @@
},
"dependencies": {
"@nodelib/fs.walk": "^3.0.1",
"better-sqlite3": "^12.5.0",
"env-paths": "^3.0.0",
"fs-extra": "^11.3.0",
"fuse.js": "^7.1.0",
Expand Down
68 changes: 43 additions & 25 deletions src/Spago/Db.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import Database from "better-sqlite3";
import { DatabaseSync } from "node:sqlite";
import fs from "node:fs";
import path from "node:path";

export const connectImpl = (path, logger) => {
logger("Connecting to database at " + path);
let db = new Database(path, {
fileMustExist: false,
// verbose: logger,
export const connectImpl = (databasePath, logger) => {
logger("Connecting to database at " + databasePath);

// Ensure directory exists
const dir = path.dirname(databasePath);
fs.mkdirSync(dir, { recursive: true });

const db = new DatabaseSync(databasePath, {
enableForeignKeyConstraints: true,
timeout: 5000, // Wait up to 5s if database is locked (matches better-sqlite3 default)
});
db.pragma("journal_mode = WAL");
db.pragma("foreign_keys = ON");

db.exec("PRAGMA journal_mode = WAL");

db.prepare(`CREATE TABLE IF NOT EXISTS package_sets
( version TEXT PRIMARY KEY NOT NULL
Expand All @@ -31,32 +38,43 @@ export const connectImpl = (path, logger) => {
, last_fetched TEXT NOT NULL
)`).run();
// it would be lovely if we'd have a foreign key on package_metadata, but that would
// require reading metadatas before manifests, which we can't always guarantee
// require reading metadata before manifests, which we can't always guarantee
db.prepare(`CREATE TABLE IF NOT EXISTS package_manifests
( name TEXT NOT NULL
, version TEXT NOT NULL
, manifest TEXT NOT NULL
, PRIMARY KEY (name, version)
)`).run();
return db;
};
}

export const insertPackageSetImpl = (db, packageSet) => {
db.prepare(
"INSERT OR IGNORE INTO package_sets (version, compiler, date) VALUES (@version, @compiler, @date)"
).run(packageSet);
};
}

export const insertPackageSetEntryImpl = (db, packageSetEntry) => {
db.prepare(
"INSERT OR IGNORE INTO package_set_entries (packageSetVersion, packageName, packageVersion) VALUES (@packageSetVersion, @packageName, @packageVersion)"
).run(packageSetEntry);
}

export const withTransactionImpl = (db, action) => {
db.exec("BEGIN IMMEDIATE");
try {
action();
db.exec("COMMIT");
} catch (e) {
db.exec("ROLLBACK");
throw e;
}
}

export const selectLatestPackageSetByCompilerImpl = (db, compiler) => {
const row = db
.prepare("SELECT * FROM package_sets WHERE compiler = ? ORDER BY date DESC LIMIT 1")
.get(compiler);
.prepare("SELECT * FROM package_sets WHERE compiler = @compiler ORDER BY date DESC LIMIT 1")
.get({ compiler });
return row;
}

Expand All @@ -69,22 +87,22 @@ export const selectPackageSetsImpl = (db) => {

export const selectPackageSetEntriesBySetImpl = (db, packageSetVersion) => {
const row = db
.prepare("SELECT * FROM package_set_entries WHERE packageSetVersion = ?")
.all(packageSetVersion);
.prepare("SELECT * FROM package_set_entries WHERE packageSetVersion = @packageSetVersion")
.all({ packageSetVersion });
return row;
}

export const selectPackageSetEntriesByPackageImpl = (db, packageName, packageVersion) => {
const row = db
.prepare("SELECT * FROM package_set_entries WHERE packageName = ? AND packageVersion = ?")
.all(packageName, packageVersion);
.prepare("SELECT * FROM package_set_entries WHERE packageName = @packageName AND packageVersion = @packageVersion")
.all({ packageName, packageVersion });
return row;
}

export const getLastPullImpl = (db, key) => {
const row = db
.prepare("SELECT * FROM last_git_pull WHERE key = ? LIMIT 1")
.get(key);
.prepare("SELECT * FROM last_git_pull WHERE key = @key LIMIT 1")
.get({ key });
return row?.date;
}

Expand All @@ -94,8 +112,8 @@ export const updateLastPullImpl = (db, key, date) => {

export const getManifestImpl = (db, name, version) => {
const row = db
.prepare("SELECT * FROM package_manifests WHERE name = ? AND version = ? LIMIT 1")
.get(name, version);
.prepare("SELECT * FROM package_manifests WHERE name = @name AND version = @version LIMIT 1")
.get({ name, version });
return row?.manifest;
}

Expand All @@ -104,7 +122,7 @@ export const insertManifestImpl = (db, name, version, manifest) => {
}

export const removeManifestImpl = (db, name, version) => {
db.prepare("DELETE FROM package_manifests WHERE name = ? AND version = ?").run(name, version);
db.prepare("DELETE FROM package_manifests WHERE name = @name AND version = @version").run({ name, version });
}

export const insertMetadataImpl = (db, name, metadata, last_fetched) => {
Expand All @@ -113,6 +131,6 @@ export const insertMetadataImpl = (db, name, metadata, last_fetched) => {

export const getMetadataForPackagesImpl = (db, names) => {
// There can be a lot of package names here, potentially hitting the max number of sqlite parameters, so we use json to bypass this
const query = db.prepare("SELECT * FROM package_metadata WHERE name IN (SELECT value FROM json_each(?));");
return query.all(JSON.stringify(names));
};
const query = db.prepare("SELECT * FROM package_metadata WHERE name IN (SELECT value FROM json_each(@names));");
return query.all({ names: JSON.stringify(names) });
}
6 changes: 6 additions & 0 deletions src/Spago/Db.purs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ module Spago.Db
, selectLatestPackageSetByCompiler
, selectPackageSets
, updateLastPull
, withTransaction
) where

import Spago.Prelude
Expand Down Expand Up @@ -60,6 +61,9 @@ insertPackageSet db = Uncurried.runEffectFn2 insertPackageSetImpl db <<< package
insertPackageSetEntry :: Db -> PackageSetEntry -> Effect Unit
insertPackageSetEntry db = Uncurried.runEffectFn2 insertPackageSetEntryImpl db <<< packageSetEntryToJs

withTransaction :: Db -> Effect Unit -> Effect Unit
withTransaction db action = Uncurried.runEffectFn2 withTransactionImpl db action

selectPackageSets :: Db -> Effect (Array PackageSet)
selectPackageSets db = do
packageSets <- Uncurried.runEffectFn1 selectPackageSetsImpl db
Expand Down Expand Up @@ -238,6 +242,8 @@ foreign import insertPackageSetImpl :: EffectFn2 Db PackageSetJs Unit

foreign import insertPackageSetEntryImpl :: EffectFn2 Db PackageSetEntryJs Unit

foreign import withTransactionImpl :: EffectFn2 Db (Effect Unit) Unit

foreign import selectLatestPackageSetByCompilerImpl :: EffectFn2 Db String (Nullable PackageSetJs)

foreign import selectPackageSetsImpl :: EffectFn1 Db (Array PackageSetJs)
Expand Down
37 changes: 37 additions & 0 deletions src/Spago/NodeVersion.purs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
module Spago.NodeVersion
( NodeVersionCheck(..)
, checkNodeVersion
) where

import Prelude

import Data.Array as Array
import Data.Int as Int
import Data.Maybe (Maybe(..), fromMaybe)
import Data.String as String
import Data.Traversable (traverse)

data NodeVersionCheck
= NodeVersionOk
| NodeVersionTooOld String
| NodeVersionUnparseable String

derive instance Eq NodeVersionCheck
instance Show NodeVersionCheck where
show NodeVersionOk = "NodeVersionOk"
show (NodeVersionTooOld v) = "(NodeVersionTooOld " <> show v <> ")"
show (NodeVersionUnparseable v) = "(NodeVersionUnparseable " <> show v <> ")"

-- | Check if a version string meets the minimum Node.js version requirement
checkNodeVersion :: { major :: Int, minor :: Int } -> String -> NodeVersionCheck
checkNodeVersion minimum version =
case traverse Int.fromString (Array.take 2 parts) of
Just [ major, minor ]
| major > minimum.major -> NodeVersionOk
| major == minimum.major && minor >= minimum.minor -> NodeVersionOk
| otherwise -> NodeVersionTooOld version
_ -> NodeVersionUnparseable version
where
-- version is like "v22.5.0" or "22.5.0"
versionStr = String.stripPrefix (String.Pattern "v") version # fromMaybe version
parts = String.split (String.Pattern ".") versionStr
7 changes: 4 additions & 3 deletions src/Spago/Registry.purs
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,10 @@ getRegistryFns registryBox registryLock = do
-- First insert the package set
logDebug $ "Inserting package set in DB: " <> Version.print setVersion
liftEffect $ Db.insertPackageSet db { compiler: set.compiler, date: set.published, version: set.version }
-- Then we insert every entry separately
for_ (Map.toUnfoldable set.packages :: Array _) \(Tuple name version) -> do
liftEffect $ Db.insertPackageSetEntry db { packageName: name, packageVersion: version, packageSetVersion: set.version }
-- Then we insert every entry in a transaction (avoids "database is locked" on Windows)
liftEffect $ Db.withTransaction db do
for_ (Map.toUnfoldable set.packages :: Array _) \(Tuple name version) -> do
Db.insertPackageSetEntry db { packageName: name, packageVersion: version, packageSetVersion: set.version }

-- | List all the package sets versions available in the Registry repo
getAvailablePackageSets :: a. Spago (LogEnv a) (Array Version)
Expand Down
2 changes: 1 addition & 1 deletion test/Prelude.purs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ withTempDir = Aff.bracket createTempDir cleanupTempDir
spago' stdin args =
Cmd.exec
(Path.global "node")
([ Path.toRaw $ oldCwd </> "bin" </> "index.dev.js" ] <> args)
([ "--no-warnings=ExperimentalWarning", Path.toRaw $ oldCwd </> "bin" </> "index.dev.js" ] <> args)
$ Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false, pipeStdin = stdin }

spago = spago' StdinNewPipe
Expand Down
2 changes: 2 additions & 0 deletions test/Spago/Unit.purs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Prelude
import Test.Spago.Unit.CheckInjectivity as CheckInjectivity
import Test.Spago.Unit.FindFlags as FindFlags
import Test.Spago.Unit.Git as Git
import Test.Spago.Unit.NodeVersion as NodeVersion
import Test.Spago.Unit.Path as Path
import Test.Spago.Unit.Printer as Printer
import Test.Spec (Spec)
Expand All @@ -17,3 +18,4 @@ spec = Spec.describe "unit" do
Printer.spec
Git.spec
Path.spec
NodeVersion.spec
40 changes: 40 additions & 0 deletions test/Spago/Unit/NodeVersion.purs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
module Test.Spago.Unit.NodeVersion where

import Prelude

import Spago.NodeVersion (NodeVersionCheck(..), checkNodeVersion)
import Test.Spec (Spec)
import Test.Spec as Spec
import Test.Spec.Assertions as Assertions

minimum :: { major :: Int, minor :: Int }
minimum = { major: 22, minor: 5 }

spec :: Spec Unit
spec = do
Spec.describe "checkNodeVersion" do
Spec.describe "accepts valid versions" do
Spec.it "v22.5.0" do
checkNodeVersion minimum "v22.5.0" `Assertions.shouldEqual` NodeVersionOk
Spec.it "22.5.0" do
checkNodeVersion minimum "22.5.0" `Assertions.shouldEqual` NodeVersionOk
Spec.it "v22.6.0" do
checkNodeVersion minimum "v22.6.0" `Assertions.shouldEqual` NodeVersionOk
Spec.it "v23.0.0" do
checkNodeVersion minimum "v23.0.0" `Assertions.shouldEqual` NodeVersionOk
Spec.it "v25.2.1" do
checkNodeVersion minimum "v25.2.1" `Assertions.shouldEqual` NodeVersionOk

Spec.describe "rejects old versions" do
Spec.it "v22.4.0" do
checkNodeVersion minimum "v22.4.0" `Assertions.shouldEqual` NodeVersionTooOld "v22.4.0"
Spec.it "v21.0.0" do
checkNodeVersion minimum "v21.0.0" `Assertions.shouldEqual` NodeVersionTooOld "v21.0.0"
Spec.it "v18.17.0" do
checkNodeVersion minimum "v18.17.0" `Assertions.shouldEqual` NodeVersionTooOld "v18.17.0"

Spec.describe "handles unparseable versions" do
Spec.it "garbage" do
checkNodeVersion minimum "garbage" `Assertions.shouldEqual` NodeVersionUnparseable "garbage"
Spec.it "empty string" do
checkNodeVersion minimum "" `Assertions.shouldEqual` NodeVersionUnparseable ""
Loading