Skip to content

Commit

Permalink
Implement package manager abstraction
Browse files Browse the repository at this point in the history
  • Loading branch information
ptrdom committed May 21, 2022
1 parent 00460f5 commit 4ef34f7
Show file tree
Hide file tree
Showing 24 changed files with 494 additions and 28 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: 16.14.2
- name: Enable Corepack
run: corepack enable
- name: Setup yarn
run: npm install -g [email protected]
- name: Setup pnpm
run: npm install -g [email protected]
- name: Unit tests
run: sbt test
- name: Scripted tests
Expand Down
22 changes: 14 additions & 8 deletions manual/src/ornate/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,24 @@ their execution so that they can be loaded by jsdom.
You can find an example of project requiring the DOM for its tests
[here](https://github.com/scalacenter/scalajs-bundler/blob/main/sbt-scalajs-bundler/src/sbt-test/sbt-scalajs-bundler/static/).

### Yarn {#yarn}
### Package managers

By default, `npm` is used to fetch the dependencies but you can use [Yarn](https://yarnpkg.com/) by setting the
`useYarn` key to `true`:
By default, `npm` is used to fetch the dependencies, but you can use [Yarn](https://yarnpkg.com/) by setting the
`jsPackageManager` key to `Yarn()`:

~~~ scala
useYarn := true
~~~
``` scala
jsPackageManager := Yarn()
```

If your sbt (sub-)project directory contains a lockfile (`package-lock.json` for `npm` or `yarn.lock` for `yarn`), it will be used. Else, a new one will be created.
You should check the lockfile into source control.

If your sbt (sub-)project directory contains a `yarn.lock`, it will be used. Else, a new one will be created. You should check `yarn.lock` into source control.
Package manager behavior can be customized by passing your own [PackageManager](api:scalajsbundler.PackageManager) to the key.
You can use it to modify commands and their arguments for `npm` or `yarn`, or to set up new package managers like
[pnpm](https://pnpm.io/) (see example [here](https://github.com/scalacenter/scalajs-bundler/blob/main/sbt-scalajs-bundler/src/sbt-test/sbt-scalajs-bundler/pnpm/)).

Yarn 0.22.0+ must be available on your machine.
Scalajs-bundler does not install your chosen package manager, it must be available on your machine. However, [Corepack](https://nodejs.org/api/corepack.html)
is supported - setting `Yarn.withVersion(Some("1.22.19"))` will modify underlying `package.json` with field `"packageManager": "[email protected]"`.

### Bundling Mode {#bundling-mode}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import scalajsbundler.util.Commands
*
* @param name Name of the command to run
*/
@deprecated("Use jsPackageManager instead.")
class ExternalCommand(name: String) {

/**
Expand All @@ -32,6 +33,7 @@ object Npm extends ExternalCommand("npm")

object Yarn extends ExternalCommand("yarn")

@deprecated("Use jsPackageManager instead.")
object ExternalCommand {
private val yarnOptions = List("--non-interactive", "--mutex", "network")

Expand Down Expand Up @@ -89,6 +91,7 @@ object ExternalCommand {
* @param npmExtraArgs Additional arguments to pass to npm
* @param npmPackages Packages to install (e.g. "webpack", "[email protected]")
*/
@deprecated("Use jsPackageManager instead.")
def addPackages(baseDir: File,
installDir: File,
useYarn: Boolean,
Expand All @@ -107,6 +110,7 @@ object ExternalCommand {
}
}

@deprecated("Use jsPackageManager instead.")
def install(baseDir: File,
installDir: File,
useYarn: Boolean,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ object PackageJson {
currentConfiguration: Configuration,
webpackVersion: String,
webpackDevServerVersion: String,
webpackCliVersion: String
webpackCliVersion: String,
packageManager: PackageManager
): Unit = {
val npmManifestDependencies = NpmDependencies.collectFromClasspath(fullClasspath)
val dependencies =
Expand Down Expand Up @@ -62,7 +63,7 @@ object PackageJson {
val packageJson =
JSON.obj(
(
additionalNpmConfig.toSeq :+
(additionalNpmConfig.toSeq ++ packageManager.packageJsonContents.toSeq) :+
"dependencies" -> JSON.objStr(resolveDependencies(dependencies, npmResolutions, log)) :+
"devDependencies" -> JSON.objStr(resolveDependencies(devDependencies, npmResolutions, log))
): _*
Expand Down
252 changes: 252 additions & 0 deletions sbt-scalajs-bundler/src/main/scala/scalajsbundler/PackageManager.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
package scalajsbundler

import java.io.File

import sbt._
import scalajsbundler.util.Commands
import scalajsbundler.util.JSON

trait PackageManager {

val name: String

/**
* Runs the command `cmd`
* @param args Command arguments
* @param workingDir Working directory of the process
* @param logger Logger
*/
def run(args: String*)(workingDir: File, logger: Logger): Unit

def install(baseDir: File, installDir: File, logger: Logger): Unit

val packageJsonContents: Map[String, JSON]
}

object PackageManager {

abstract class ExternalProcess(
val name: String,
val installCommand: String,
val installArgs: Seq[String]
) extends PackageManager {

def run(args: String*)(workingDir: File, logger: Logger): Unit =
Commands.run(cmd ++: args, workingDir, logger)

private val cmd = sys.props("os.name").toLowerCase match {
case os if os.contains("win") => Seq("cmd", "/c", name)
case _ => Seq(name)
}

def install(baseDir: File, installDir: File, logger: Logger): Unit = {
this match {
case lfs: LockFileSupport =>
lfs.lockFileRead(baseDir, installDir, logger)
case _ =>
()
}

run(installCommand +: installArgs: _*)(installDir, logger)

this match {
case lfs: LockFileSupport =>
lfs.lockFileWrite(baseDir, installDir, logger)
case _ =>
()
}
}
}

trait AddPackagesSupport { this: PackageManager =>

val addPackagesCommand: String
val addPackagesArgs: Seq[String]

/**
* Locally install NPM packages
*
* @param baseDir The (sub-)project directory which contains yarn.lock
* @param installDir The directory in which to install the packages
* @param logger sbt logger
* @param npmPackages Packages to install (e.g. "webpack", "[email protected]")
*/
def addPackages(baseDir: File,
installDir: File,
logger: Logger,
)(npmPackages: String*): Unit = {
this match {
case lfs: LockFileSupport =>
lfs.lockFileRead(baseDir, installDir, logger)
case _ =>
()
}

run(addPackagesCommand +: (addPackagesArgs ++ npmPackages): _*)(installDir, logger)

this match {
case lfs: LockFileSupport =>
lfs.lockFileWrite(baseDir, installDir, logger)
case _ =>
()
}
}
}

trait LockFileSupport {
val lockFileName: String

def lockFileRead(
baseDir: File,
installDir: File,
logger: Logger
): Unit = {
val sourceLockFile = baseDir / lockFileName
val targetLockFile = installDir / lockFileName

if (sourceLockFile.exists()) {
logger.info("Using lockfile " + sourceLockFile)
IO.copyFile(sourceLockFile, targetLockFile)
}
}

def lockFileWrite(
baseDir: File,
installDir: File,
logger: Logger
): Unit = {
val sourceLockFile = baseDir / lockFileName
val targetLockFile = installDir / lockFileName

if (targetLockFile.exists()) {
logger.debug("Wrote lockfile to " + sourceLockFile)
IO.copyFile(targetLockFile, sourceLockFile)
}
}
}

final class Npm private (
override val name: String,
val lockFileName: String,
override val installCommand: String,
override val installArgs: Seq[String],
val addPackagesCommand: String,
val addPackagesArgs: Seq[String],
) extends ExternalProcess(name, installCommand, installArgs)
with LockFileSupport
with AddPackagesSupport {

override val packageJsonContents: Map[String, JSON] = Map.empty

private def this() = {
this(
name = "npm",
lockFileName = "package-lock.json",
installCommand = "install",
installArgs = Seq.empty,
addPackagesCommand = "install",
addPackagesArgs = Seq.empty
)
}

def withName(name: String): Npm = copy(name = name)

def withLockFileName(lockFileName: String): Npm = copy(lockFileName = lockFileName)

def withInstallCommand(installCommand: String): Npm = copy(installCommand = installCommand)

def withInstallArgs(installArgs: Seq[String]): Npm = copy(installArgs = installArgs)

def withAddPackagesCommand(addPackagesCommand: String): Npm = copy(addPackagesCommand = addPackagesCommand)

def withAddPackagesArgs(addPackagesArgs: Seq[String]): Npm = copy(addPackagesArgs = addPackagesArgs)

private def copy(
name: String = name,
lockFileName: String = lockFileName,
installCommand: String = installCommand,
installArgs: Seq[String] = installArgs,
addPackagesCommand: String = addPackagesCommand,
addPackagesArgs: Seq[String] = addPackagesArgs
) = {
new Npm(
name,
lockFileName,
installCommand,
installArgs,
addPackagesCommand,
addPackagesArgs
)
}
}
object Npm {
def apply() = new Npm()
}

final class Yarn private (
override val name: String,
val version: Option[String],
val lockFileName: String,
override val installCommand: String,
override val installArgs: Seq[String],
val addPackagesCommand: String,
val addPackagesArgs: Seq[String],
) extends ExternalProcess(name, installCommand, installArgs)
with LockFileSupport
with AddPackagesSupport {

override val packageJsonContents: Map[String, JSON] =
version.map(v => Map("packageManager" -> JSON.str(s"$name@$v"))).getOrElse(Map.empty)

private def this() = {
this(
name = "yarn",
version = None,
lockFileName = "yarn.lock",
installCommand = "install",
installArgs = Seq.empty,
addPackagesCommand = "add",
addPackagesArgs = Seq.empty
)
}

def withName(name: String): Yarn = copy(name = name)

def withVersion(version: Option[String]): Yarn = copy(version = version)

def withLockFileName(lockFileName: String): Yarn = copy(lockFileName = lockFileName)

def withInstallCommand(installCommand: String): Yarn = copy(installCommand = installCommand)

def withInstallArgs(installArgs: Seq[String]): Yarn = copy(installArgs = installArgs)

def withAddPackagesCommand(addPackagesCommand: String): Yarn = copy(addPackagesCommand = addPackagesCommand)

def withAddPackagesArgs(addPackagesArgs: Seq[String]): Yarn = copy(addPackagesArgs = addPackagesArgs)

private def copy(
name: String = name,
version: Option[String] = version,
lockFileName: String = lockFileName,
installCommand: String = installCommand,
installArgs: Seq[String] = installArgs,
addPackagesCommand: String = addPackagesCommand,
addPackagesArgs: Seq[String] = addPackagesArgs
) = {
new Yarn(
name,
version,
lockFileName,
installCommand,
installArgs,
addPackagesCommand,
addPackagesArgs
)
}
}
object Yarn {
val DefaultArgs: Seq[String] = Seq("--non-interactive", "--mutex", "network")

def apply() = new Yarn()
}
}
Loading

0 comments on commit 4ef34f7

Please sign in to comment.