diff --git a/README.md b/README.md index 4ab7c54..1a4ab8a 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,17 @@ # Deno with packages -This flake provides utilities for managing Deno dependencies with Nix, +This flake [part](flake.parts) provides utilities for managing Deno dependencies with Nix. + +Features: + - reading multiple lock files - creating a derivation per Deno dependency - combining those into a single derivation -- installing shared cache into `DENO_DIR` +- wrapping `deno` executable to have it install the cache +- keeping cache in `/.deno_cache` Currently only NPM dependencies are recognized. ## Usage -See `flake.nix` +See `example/flake.nix` diff --git a/deno.lock b/example/deno.lock similarity index 100% rename from deno.lock rename to example/deno.lock diff --git a/example/flake.lock b/example/flake.lock new file mode 100644 index 0000000..0b5f294 --- /dev/null +++ b/example/flake.lock @@ -0,0 +1,89 @@ +{ + "nodes": { + "deno-with-packages": { + "locked": { + "path": "../", + "type": "path" + }, + "original": { + "path": "../", + "type": "path" + }, + "parent": [] + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1756770412, + "narHash": "sha256-+uWLQZccFHwqpGqr2Yt5VsW/PbeJVTn9Dk6SHWhNRPw=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "4524271976b625a4a605beefd893f270620fd751", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-root": { + "locked": { + "lastModified": 1723604017, + "narHash": "sha256-rBtQ8gg+Dn4Sx/s+pvjdq3CB2wQNzx9XGFq/JVGCB6k=", + "owner": "srid", + "repo": "flake-root", + "rev": "b759a56851e10cb13f6b8e5698af7b59c44be26e", + "type": "github" + }, + "original": { + "owner": "srid", + "repo": "flake-root", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1757745802, + "narHash": "sha256-hLEO2TPj55KcUFUU1vgtHE9UEIOjRcH/4QbmfHNF820=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "c23193b943c6c689d70ee98ce3128239ed9e32d1", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1754788789, + "narHash": "sha256-x2rJ+Ovzq0sCMpgfgGaaqgBSwY+LST+WbZ6TytnT9Rk=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "a73b9c743612e4244d865a2fdee11865283c04e6", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "root": { + "inputs": { + "deno-with-packages": "deno-with-packages", + "flake-parts": "flake-parts", + "flake-root": "flake-root", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/example/flake.nix b/example/flake.nix new file mode 100644 index 0000000..5bb20f4 --- /dev/null +++ b/example/flake.nix @@ -0,0 +1,33 @@ +{ + description = "Usage example"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-parts.url = "github:hercules-ci/flake-parts"; + flake-root.url = "github:srid/flake-root"; + # replace with "github:voleum-org/deno-with-packages" + deno-with-packages.url = "path:../"; + }; + + outputs = inputs@{ flake-parts, flake-root, deno-with-packages, ... }: + flake-parts.lib.mkFlake { inherit inputs; } { + imports = [ + flake-root.flakeModule + deno-with-packages.flakeModule + ]; + + systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; + + perSystem = { config, pkgs, ... }: { + deno = { + enable = true; + lockfiles = [ ./deno.lock ]; + }; + + devShells.default = pkgs.mkShell { + inputsFrom = [ config.flake-root.devShell ]; + buildInputs = [ config.packages.deno ]; + }; + }; + }; +} diff --git a/flake-module.nix b/flake-module.nix new file mode 100644 index 0000000..dbd9c43 --- /dev/null +++ b/flake-module.nix @@ -0,0 +1,169 @@ +{ self, lib, flake-parts-lib, ... }: +let + inherit (flake-parts-lib) mkPerSystemOption; + inherit (lib) mkOption mkEnableOption types; +in +{ + options = { + flake.lib.deno = mkOption { + type = types.attrs; + default = {}; + description = "Deno-related library functions"; + }; + }; + + options.perSystem = mkPerSystemOption ({ pkgs, system, ... }: { + options.deno = { + enable = mkEnableOption "Deno with shared cache support"; + + lockfiles = mkOption { + type = types.listOf types.path; + default = []; + description = "List of deno.lock files to build cache from"; + }; + + package = mkOption { + type = types.package; + default = pkgs.deno; + description = "Base deno package to wrap"; + }; + + registryHostname = mkOption { + type = types.str; + default = "registry.npmjs.org"; + description = "NPM registry hostname"; + }; + }; + }); + + config = { + flake.lib.deno = rec { + denoLockfileToNpmDeps = { pkgs, lockfile, registryHostname }: + let + extractNpmDeps = lockfile: + let + lockJSON = builtins.fromJSON (builtins.readFile lockfile); + in + lockJSON.npm or {}; + + npmDeps = extractNpmDeps lockfile; + + mkTarballDrv = { name, version, integrity }: + let + tarballName = + if builtins.substring 0 1 name == "@" + then builtins.baseNameOf name # "@types/node" -> "node" + else name; + in + pkgs.fetchurl { + url = "https://${registryHostname}/${name}/-/${tarballName}-${version}.tgz"; + hash = integrity; + }; + + mkNpmDep = key: value: + let + match = builtins.match "(@?[^@]+)@([^_]+).*" key; + name = builtins.elemAt match 0; + version = builtins.elemAt match 1; + in + pkgs.stdenv.mkDerivation { + pname = "deno-npm-${builtins.replaceStrings ["@" "/"] ["" "-"] name}"; + version = version; + src = mkTarballDrv { + inherit name version; + inherit (value) integrity; + }; + + installPhase = '' + runHook preInstall + + export OUT_PATH="$out/npm/${registryHostname}/${name}/${version}" + + mkdir -p $OUT_PATH + tar -xzf $src -C $OUT_PATH --strip-components=1 + + SUBSET_FILTER='to_entries | map(select(.key == "version" or .key == "bin" or .key == "dependencies" or .key == "peerDependencies")) | from_entries' + PACKAGE_SUBSET=$(${pkgs.jq}/bin/jq "$SUBSET_FILTER" $OUT_PATH/package.json) + + cat > $OUT_PATH/../registry.json <&2 + exit 1 + fi + + if [ ! -d "$CACHE_DIR" ] || [ ! "$(ls -A "$CACHE_DIR" 2>/dev/null)" ]; then + echo "Setting up Deno cache at: $CACHE_DIR" >&2 + mkdir -p "$CACHE_DIR" + + if [ -d "${sharedCache}" ]; then + echo "Copying shared cache..." >&2 + cp -rL "${sharedCache}"/* "$CACHE_DIR"/ 2>/dev/null || true + chmod -R u+w "$CACHE_DIR" 2>/dev/null || true + fi + fi + + export DENO_DIR="$CACHE_DIR" + exec "${baseDeno}/bin/deno" "$@" + ''; + }; + + perSystem = { config, pkgs, system, ... }: + lib.mkIf config.deno.enable { + packages = lib.mkIf (config.deno.lockfiles != []) { + deno = + let + sharedCache = self.lib.deno.denoSharedCache { + inherit pkgs; + lockfiles = config.deno.lockfiles; + registryHostname = config.deno.registryHostname; + }; + in + self.lib.deno.denoWithCache { + inherit pkgs sharedCache; + baseDeno = config.deno.package; + }; + + deno-cache = self.lib.deno.denoSharedCache { + inherit pkgs; + lockfiles = config.deno.lockfiles; + registryHostname = config.deno.registryHostname; + }; + }; + }; + }; +} diff --git a/flake.lock b/flake.lock deleted file mode 100644 index 720aa25..0000000 --- a/flake.lock +++ /dev/null @@ -1,61 +0,0 @@ -{ - "nodes": { - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1754214453, - "narHash": "sha256-Q/I2xJn/j1wpkGhWkQnm20nShYnG7TI99foDBpXm1SY=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "5b09dc45f24cf32316283e62aec81ffee3c3e376", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix index 84123fa..a9f186c 100644 --- a/flake.nix +++ b/flake.nix @@ -1,59 +1,7 @@ { description = "Builds a shared cache for Deno packages based on lock files"; - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - flake-utils.url = "github:numtide/flake-utils"; + outputs = { ... }: { + flakeModule = ./flake-module.nix; }; - - outputs = { self, nixpkgs, flake-utils }: - flake-utils.lib.eachDefaultSystem (system: - let - pkgs = nixpkgs.legacyPackages.${system}; - in rec - { - lib = { - # Extract NPM dependencies from a single Deno lock file - denoLockfileToNpmDeps = import ./lib/lockfile-to-npm-deps.nix { self = lib; }; - - # Create a shared Deno cache from multiple lock files - denoSharedCache = import ./lib/shared-cache.nix { self = lib; }; - - # Install shared cache into DENO_DIR - installDenoCache = import ./lib/install-cache.nix; - - denoNpmRegistryHostname = "registry.npmjs.org"; - }; - - devShells.default = pkgs.mkShell { - buildInputs = with pkgs; [ - deno - ]; - - shellHook = - let - # Example - sharedCache = self.lib.${system}.denoSharedCache { - inherit pkgs; - lockfiles = [ ./deno.lock ]; # Add your lock files here - }; - installScript = self.lib.${system}.installDenoCache { - inherit pkgs; - cache = sharedCache; - }; - in - '' - export DENO_DIR="$PWD/.deno_cache" - - if [ -d "${sharedCache}" ]; then - echo "Installing Deno shared cache..." - ${installScript}/bin/install-deno-cache - fi - - echo "Deno development environment ready!" - echo "DENO_DIR is set to: $DENO_DIR" - ''; - }; - } - ); } diff --git a/lib/install-cache.nix b/lib/install-cache.nix deleted file mode 100644 index fc28906..0000000 --- a/lib/install-cache.nix +++ /dev/null @@ -1,34 +0,0 @@ -{ pkgs, cache }: - -pkgs.writeShellScriptBin "install-deno-cache" '' - set -euo pipefail - - if [ -z "''${DENO_DIR:-}" ]; then - echo "Error: DENO_DIR environment variable must be set" >&2 - exit 1 - fi - - echo "Installing Deno cache to: $DENO_DIR" - - # Remove existing cache if it exists - if [ -d "$DENO_DIR" ]; then - echo "Removing existing cache..." - rm -rf "$DENO_DIR" - fi - - # Copy the shared cache, dereferencing symlinks - if [ -d "${cache}" ]; then - echo "Copying shared cache..." - mkdir -p "$(dirname "$DENO_DIR")" - cp -rL "${cache}" "$DENO_DIR" - chmod -R u+w "$DENO_DIR" - echo "Cache installed successfully!" - else - echo "No shared cache found at ${cache}" - mkdir -p "$DENO_DIR" - fi - - if [ -d "$DENO_DIR/npm" ]; then - echo "NPM packages in cache: $(find "$DENO_DIR/npm" -name "package.json" | wc -l)" - fi -'' diff --git a/lib/lockfile-to-npm-deps.nix b/lib/lockfile-to-npm-deps.nix deleted file mode 100644 index be9b21c..0000000 --- a/lib/lockfile-to-npm-deps.nix +++ /dev/null @@ -1,69 +0,0 @@ -{ self }: -{ pkgs, lockfile }: -let - registry = self.denoNpmRegistryHostname; - - # Extract npm dependencies from a single lockfile - extractNpmDeps = lockfile: - let - lockJSON = builtins.fromJSON (builtins.readFile lockfile); - in - lockJSON.npm or {}; - - npmDeps = extractNpmDeps lockfile; - - # Create tarball derivation for a package - mkTarballDrv = { name, version, integrity }: - let - tarballName = - if builtins.substring 0 1 name == "@" - then builtins.baseNameOf name # "@types/node" → "node" - else name; - in - pkgs.fetchurl { - url = "https://${registry}/${name}/-/${tarballName}-${version}.tgz"; - hash = integrity; - }; - - # Create derivation for a single npm package - mkNpmDep = key: value: - let - match = builtins.match "(@?[^@]+)@([^_]+).*" key; - name = builtins.elemAt match 0; - version = builtins.elemAt match 1; - in - pkgs.stdenv.mkDerivation { - pname = "deno-npm-${builtins.replaceStrings ["@" "/"] ["" "-"] name}"; - version = version; - src = mkTarballDrv { - inherit name version; - inherit (value) integrity; - }; - - installPhase = '' - runHook preInstall - - export OUT_PATH="$out/npm/${registry}/${name}/${version}" - - mkdir -p $OUT_PATH - tar -xzf $src -C $OUT_PATH --strip-components=1 - - SUBSET_FILTER='to_entries | map(select(.key == "version" or .key == "bin" or .key == "dependencies" or .key == "peerDependencies")) | from_entries' - PACKAGE_SUBSET=$(${pkgs.jq}/bin/jq "$SUBSET_FILTER" $OUT_PATH/package.json) - - cat > $OUT_PATH/../registry.json <