-
Notifications
You must be signed in to change notification settings - Fork 101
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement package manager abstraction
- Loading branch information
Showing
24 changed files
with
494 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ import scalajsbundler.util.Commands | |
* | ||
* @param name Name of the command to run | ||
*/ | ||
@deprecated("Use jsPackageManager instead.") | ||
class ExternalCommand(name: String) { | ||
|
||
/** | ||
|
@@ -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") | ||
|
||
|
@@ -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, | ||
|
@@ -107,6 +110,7 @@ object ExternalCommand { | |
} | ||
} | ||
|
||
@deprecated("Use jsPackageManager instead.") | ||
def install(baseDir: File, | ||
installDir: File, | ||
useYarn: Boolean, | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
252 changes: 252 additions & 0 deletions
252
sbt-scalajs-bundler/src/main/scala/scalajsbundler/PackageManager.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
Oops, something went wrong.