-
Notifications
You must be signed in to change notification settings - Fork 101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Package manager abstraction #420
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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] | ||
run: corepack prepare [email protected] --activate | ||
- name: Setup pnpm | ||
run: corepack prepare [email protected] --activate | ||
- name: Unit tests | ||
run: sbt test | ||
- name: Scripted tests | ||
|
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} | ||
|
||
|
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should keep all of this as |
||
|
||
/** | ||
|
@@ -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, | ||
|
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be better to convert this to a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Though I do not have the best idea on where to put |
||
|
||
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() | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Managing package manager installations inside and outside Corepack do not mix well, so we got to do it with Corepack only.