Skip to content

Commit

Permalink
feat: limited pnp install strategy support
Browse files Browse the repository at this point in the history
  • Loading branch information
elbywan committed Oct 1, 2023
1 parent e232e2c commit e851193
Show file tree
Hide file tree
Showing 34 changed files with 13,333 additions and 48 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,15 @@ zap --help

Here is a non exhaustive list of features that are currently implemented:

- **Classic (npm-like) or isolated (pnpm-like) install strategies**
- **Classic (~npm), isolated (~pnpm) or plug'n'play (~yarn) install strategies**

```bash
# Classic install by default
zap i # or zap i --classic
# Isolated install
zap i --isolated
# Plug'n'play (experimental - no zero-installs yet)
zap i --pnp
```

_or:_
Expand Down
5 changes: 5 additions & 0 deletions src/cli/install.cr
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class Zap::CLI
Possible values:
- classic (default) : mimics the behavior of npm and yarn: install non-duplicated in top-level, and duplicated as necessary within directory structure.
- isolated : mimics the behavior of pnpm: dependencies are symlinked from a virtual store at node_modules/.zap.
- pnp : a limited plug'n'play approach similar to yarn
- classic_shallow : like classic but will only install direct dependencies at top-level.
DESCRIPTION
) do |strategy|
Expand All @@ -51,6 +52,10 @@ class Zap::CLI
@command_config = install_config.copy_with(strategy: InstallConfig::InstallStrategy::Isolated)
end

parser.on("--pnp", "Shorthand for: --install-strategy pnp") do
@command_config = install_config.copy_with(strategy: InstallConfig::InstallStrategy::Pnp)
end

subSeparator("Save")

unless update_packages
Expand Down
1 change: 1 addition & 0 deletions src/commands/install/config.cr
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ struct Zap::Commands::Install::Config < Zap::Commands::Config
Classic
Classic_Shallow
Isolated
Pnp
end

# Configuration specific for the install command
Expand Down
3 changes: 3 additions & 0 deletions src/commands/install/install.cr
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ require "../../npmrc"
require "../../resolver"
require "../../installer/isolated"
require "../../installer/classic"
require "../../installer/pnp"
require "../../workspaces"

module Zap::Commands::Install
Expand Down Expand Up @@ -310,6 +311,8 @@ module Zap::Commands::Install
Installer::Isolated.new(state)
when .classic?, .classic_shallow?
Installer::Classic.new(state)
when .pnp?
Installer::PnP.new(state)
else
raise "Unsupported install strategy: #{state.install_config.strategy}"
end
Expand Down
9 changes: 9 additions & 0 deletions src/config.cr
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@ module Zap
nodejs.try { |node_bin| Path.new(node_bin).dirname }
end

def pnp_runtime? : Path?
runtime_path = Path.new(prefix, ".pnp.cjs")
if ::File.exists?(runtime_path)
runtime_path
else
nil
end
end

def deduce_global_prefix : String
begin
{% if flag?(:windows) %}
Expand Down
15 changes: 7 additions & 8 deletions src/installer/backend/backend.cr
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,26 @@ module Zap::Backend
Symlink
end

def self.install(*, dependency : Package, target : Path | String, backend : Backends, store : Store, aliased_name : String? = nil, &on_installing) : Bool
def self.install(*, dependency : Package, target : Path | String, backend : Backends, store : Store, &on_installing) : Bool
case backend
in .clone_file?
{% if flag?(:darwin) %}
Backend::CloneFile.install(dependency, target, store: store, aliased_name: aliased_name, &on_installing)
Backend::CloneFile.install(dependency, target, store: store, &on_installing)
{% else %}
raise "The clonefile backend is not supported on this platform"
{% end %}
in .copy_file?
{% if flag?(:darwin) %}
Backend::CopyFile.install(dependency, target, store: store, aliased_name: aliased_name, &on_installing)
Backend::CopyFile.install(dependency, target, store: store, &on_installing)
{% else %}
raise "The copyfile backend is not supported on this platform"
{% end %}
in .hardlink?
Backend::Hardlink.install(dependency, target, store: store, aliased_name: aliased_name, &on_installing)
Backend::Hardlink.install(dependency, target, store: store, &on_installing)
in .copy?
Backend::Copy.install(dependency, target, store: store, aliased_name: aliased_name, &on_installing)
Backend::Copy.install(dependency, target, store: store, &on_installing)
in .symlink?
Backend::Symlink.install(dependency, target, store: store, aliased_name: aliased_name, &on_installing)
Backend::Symlink.install(dependency, target, store: store, &on_installing)
end
end

Expand Down Expand Up @@ -116,9 +116,8 @@ module Zap::Backend
# end
# end

protected def self.prepare(dependency : Package, node_modules : Path | String, *, store : Store, mkdir_parent = false, aliased_name : String? = nil) : {Path, Path, Bool}
protected def self.prepare(dependency : Package, dest_path : Path | String, *, store : Store, mkdir_parent = false) : {Path, Path, Bool}
src_path = store.package_path(dependency.name, dependency.version)
dest_path = node_modules / (aliased_name || dependency.name)
already_installed = Installer.package_already_installed?(dependency, dest_path)
Utils::Directories.mkdir_p(mkdir_parent ? dest_path.dirname : dest_path) unless already_installed
{src_path, dest_path, already_installed}
Expand Down
4 changes: 2 additions & 2 deletions src/installer/backend/clonefile.cr
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module Zap::Backend
module CloneFile
def self.install(dependency : Package, target : Path, *, store : Store, aliased_name : String? = nil, &on_installing) : Bool
src_path, dest_path, already_installed = Backend.prepare(dependency, target, store: store, mkdir_parent: true, aliased_name: aliased_name)
def self.install(dependency : Package, target : Path, *, store : Store, &on_installing) : Bool
src_path, dest_path, already_installed = Backend.prepare(dependency, target, store: store, mkdir_parent: true)
return false if already_installed
yield
result = LibC.clonefile(src_path.to_s, dest_path.to_s, 0)
Expand Down
4 changes: 2 additions & 2 deletions src/installer/backend/copy.cr
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module Zap::Backend
module Copy
def self.install(dependency : Package, target : Path, *, store : Store, aliased_name : String? = nil, &on_installing) : Bool
src_path, dest_path, already_installed = Backend.prepare(dependency, target, store: store, aliased_name: aliased_name)
def self.install(dependency : Package, target : Path, *, store : Store, &on_installing) : Bool
src_path, dest_path, already_installed = Backend.prepare(dependency, target, store: store)
return false if already_installed
yield
# FileUtils.cp_r(src_path, dest_path)
Expand Down
4 changes: 2 additions & 2 deletions src/installer/backend/copyfile.cr
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module Zap::Backend
module CopyFile
def self.install(dependency : Package, target : Path, *, store : Store, aliased_name : String? = nil, &on_installing) : Bool
src_path, dest_path, already_installed = Backend.prepare(dependency, target, store: store, aliased_name: aliased_name)
def self.install(dependency : Package, target : Path, *, store : Store, &on_installing) : Bool
src_path, dest_path, already_installed = Backend.prepare(dependency, target, store: store)
return false if already_installed
yield
Pipeline.wrap(force_wait: true) do |pipeline|
Expand Down
2 changes: 1 addition & 1 deletion src/installer/backend/hardlink.cr
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module Zap::Backend
module Hardlink
def self.install(dependency : Package, target : Path, *, store : Store, aliased_name : String? = nil, &on_installing) : Bool
src_path, dest_path, already_installed = Backend.prepare(dependency, target, store: store, aliased_name: aliased_name)
src_path, dest_path, already_installed = Backend.prepare(dependency, target, store: store)
return false if already_installed
yield
Pipeline.wrap(force_wait: true) do |pipeline|
Expand Down
2 changes: 1 addition & 1 deletion src/installer/backend/symlink.cr
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module Zap::Backend
module Symlink
def self.install(dependency : Package, target : Path, *, store : Store, aliased_name : String? = nil, &on_installing) : Bool
src_path, dest_path, already_installed = Backend.prepare(dependency, target, store: store, aliased_name: aliased_name)
src_path, dest_path, already_installed = Backend.prepare(dependency, target, store: store)
return false if already_installed
yield
Pipeline.wrap do |pipeline|
Expand Down
2 changes: 1 addition & 1 deletion src/installer/classic/writer/file.cr
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class Zap::Installer::Classic
install_folder = aliased_name || dependency.name
target_path = location.value.node_modules / install_folder
exists = Zap::Installer.package_already_installed?(dependency, target_path)
install_location = self.class.init_location(dependency, target_path, location, aliased_name)
install_location = self.class.init_location(dependency, target_path, location)

if exists
{install_location, false}
Expand Down
2 changes: 1 addition & 1 deletion src/installer/classic/writer/git.cr
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class Zap::Installer::Classic
install_folder = aliased_name || dependency.name
target_path = location.value.node_modules / install_folder
exists = Zap::Installer.package_already_installed?(dependency, target_path)
install_location = self.class.init_location(dependency, target_path, location, aliased_name)
install_location = self.class.init_location(dependency, target_path, location)

if exists
{install_location, false}
Expand Down
8 changes: 4 additions & 4 deletions src/installer/classic/writer/registry.cr
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,19 @@ class Zap::Installer::Classic
end

def install : InstallResult
installation_path = location.value.node_modules / (aliased_name || dependency.name)
installed = begin
Backend.install(dependency: dependency, target: location.value.node_modules, store: state.store, backend: state.config.file_backend, aliased_name: aliased_name) {
Backend.install(dependency: dependency, target: installation_path, store: state.store, backend: state.config.file_backend) {
state.reporter.on_installing_package
}
rescue ex
state.reporter.log(%(#{aliased_name.try &.+(":")}#{(dependency.name + '@' + dependency.version).colorize.yellow} Failed to install with #{state.config.file_backend} backend: #{ex.message}))
# Fallback to the widely supported "plain copy" backend
Backend.install(backend: :copy, dependency: dependency, target: location.value.node_modules, store: state.store, aliased_name: aliased_name) { }
Backend.install(backend: :copy, dependency: dependency, target: installation_path, store: state.store) { }
end

installation_path = location.value.node_modules / (aliased_name || dependency.name)
installer.on_install(dependency, installation_path, state: state, location: location, ancestors: ancestors) if installed
{self.class.init_location(dependency, installation_path, location, aliased_name), installed}
{self.class.init_location(dependency, installation_path, location), installed}
end

private def skip_hoisting? : Bool
Expand Down
2 changes: 1 addition & 1 deletion src/installer/classic/writer/tarball.cr
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class Zap::Installer::Classic
install_folder = aliased_name || dependency.name
target_path = location.value.node_modules / install_folder
exists = Zap::Installer.package_already_installed?(dependency, target_path)
install_location = self.class.init_location(dependency, target_path, location, aliased_name)
install_location = self.class.init_location(dependency, target_path, location)

if exists
{install_location, false}
Expand Down
2 changes: 1 addition & 1 deletion src/installer/classic/writer/writer.cr
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ abstract struct Zap::Installer::Classic::Writer

abstract def install : InstallResult

def self.init_location(dependency : Package, target_path : Path, location : LocationNode, aliased_name : String? = nil) : LocationNode
def self.init_location(dependency : Package, target_path : Path, location : LocationNode) : LocationNode
LocationNode.new(
node_modules: target_path / "node_modules",
package: dependency,
Expand Down
16 changes: 10 additions & 6 deletions src/installer/isolated/isolated.cr
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ class Zap::Installer::Isolated < Zap::Installer::Base
end

# If the package folder exists, we assume that the package dependencies were already installed too
package_path = install_path / package.name
if File.directory?(install_path)
package_path = install_path / package.name
# If there is no need to perform a full pass, we can just return the package path and skip the dependencies
unless state.install_config.refresh_install
Log.debug { "(#{package.name}) Already installed to folder '#{install_path}', skipping…" }
Expand All @@ -125,13 +125,13 @@ class Zap::Installer::Isolated < Zap::Installer::Base
Utils::Directories.mkdir_p(install_path)
case package.kind
when .tarball_file?
Writer::File.install(package, install_path, installer: self, state: state)
Writer::File.install(package, package_path, installer: self, state: state)
when .tarball_url?
Writer::Tarball.install(package, install_path, installer: self, state: state)
Writer::Tarball.install(package, package_path, installer: self, state: state)
when .git?
Writer::Git.install(package, install_path, installer: self, state: state)
Writer::Git.install(package, package_path, installer: self, state: state)
when .registry?
Writer::Registry.install(package, install_path, installer: self, state: state)
Writer::Registry.install(package, package_path, installer: self, state: state)
end
end
else
Expand All @@ -154,7 +154,11 @@ class Zap::Installer::Isolated < Zap::Installer::Base
end

# For each resolved peer and pinned dependency, install the dependency in the .store folder if it's not already installed
(resolved_peers.try(&.map { |p| {p.name, p, Package::DependencyType::Dependency} }.+ pinned_packages) || pinned_packages).each do |(name, dependency, type)|
if resolved_peers
pinned_packages + resolved_peers.map { |p| {p.name, p, Package::DependencyType::Dependency} }
else
pinned_packages
end.each do |(name, dependency, type)|
Log.debug { "(#{package.is_a?(Package) ? package.key : package.name}) Processing dependency: #{dependency.key}" }
# Add to the ancestors
ancestors.unshift(package)
Expand Down
7 changes: 3 additions & 4 deletions src/installer/isolated/writer/file.cr
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
module Zap::Installer::Isolated::Writer::File
def self.install(dependency : Package, install_path : Path, *, installer : Zap::Installer::Base, state : Commands::Install::State)
full_path = install_path / dependency.name
case dist = dependency.dist
when Package::TarballDist
exists = Zap::Installer.package_already_installed?(dependency, full_path)
exists = Zap::Installer.package_already_installed?(dependency, install_path)
unless exists
extracted_folder = Path.new(dist.path)
state.reporter.on_installing_package

FileUtils.cp_r(extracted_folder, full_path)
installer.on_install(dependency, full_path, state: state)
FileUtils.cp_r(extracted_folder, install_path)
installer.on_install(dependency, install_path, state: state)
end
else
raise "Unknown dist type: #{dist}"
Expand Down
7 changes: 3 additions & 4 deletions src/installer/isolated/writer/git.cr
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
module Zap::Installer::Isolated::Writer::Git
def self.install(dependency : Package, install_path : Path, *, installer : Zap::Installer::Base, state : Commands::Install::State)
full_path = install_path / dependency.name
unless packed_tarball_path = dependency.dist.try &.as(Package::GitDist).cache_key.try { |key| state.store.package_path(dependency.name, key + ".tgz") }
raise "Cannot install git dependency #{dependency.name} because the dist.cache_key field is missing."
end

exists = Zap::Installer.package_already_installed?(dependency, full_path)
exists = Zap::Installer.package_already_installed?(dependency, install_path)

unless exists
state.reporter.on_installing_package
::File.open(packed_tarball_path, "r") do |tarball|
Utils::TarGzip.unpack_to(tarball, full_path)
Utils::TarGzip.unpack_to(tarball, install_path)
end
installer.on_install(dependency, full_path, state: state)
installer.on_install(dependency, install_path, state: state)
end
end
end
3 changes: 1 addition & 2 deletions src/installer/isolated/writer/registry.cr
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
module Zap::Installer::Isolated::Writer::Registry
def self.install(dependency : Package, install_path : Path, *, installer : Zap::Installer::Base, state : Commands::Install::State)
full_path = install_path / dependency.name
installed = begin
Backend.install(dependency: dependency, target: install_path, store: state.store, backend: state.config.file_backend) {
state.reporter.on_installing_package
Expand All @@ -11,6 +10,6 @@ module Zap::Installer::Isolated::Writer::Registry
Backend.install(backend: :copy, dependency: dependency, target: install_path, store: state.store) { }
end

installer.on_install(dependency, full_path, state: state) if installed
installer.on_install(dependency, install_path, state: state) if installed
end
end
7 changes: 3 additions & 4 deletions src/installer/isolated/writer/tarball.cr
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
module Zap::Installer::Isolated::Writer::Tarball
def self.install(dependency : Package, install_path : Path, *, installer : Zap::Installer::Base, state : Commands::Install::State)
full_path = install_path / dependency.name
case dist = dependency.dist
when Package::TarballDist
exists = Zap::Installer.package_already_installed?(dependency, full_path)
exists = Zap::Installer.package_already_installed?(dependency, install_path)
unless exists
extracted_folder = Path.new(dist.path)
state.reporter.on_installing_package

FileUtils.cp_r(extracted_folder, full_path)
installer.on_install(dependency, full_path, state: state)
FileUtils.cp_r(extracted_folder, install_path)
installer.on_install(dependency, install_path, state: state)
end
else
raise "Unknown dist type: #{dist}"
Expand Down
Loading

0 comments on commit e851193

Please sign in to comment.