Skip to content

Commit e7413d9

Browse files
committed
Implement package manager abstraction
1 parent 00460f5 commit e7413d9

File tree

24 files changed

+495
-29
lines changed

24 files changed

+495
-29
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,12 @@ jobs:
2121
- uses: actions/setup-node@v3
2222
with:
2323
node-version: 16.14.2
24+
- name: Enable Corepack
25+
run: corepack enable
2426
- name: Setup yarn
25-
run: npm install -g [email protected]
27+
run: npm install -g [email protected] --force
28+
- name: Setup pnpm
29+
run: npm install -g [email protected] --force
2630
- name: Unit tests
2731
run: sbt test
2832
- name: Scripted tests

manual/src/ornate/reference.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,24 @@ their execution so that they can be loaded by jsdom.
5555
You can find an example of project requiring the DOM for its tests
5656
[here](https://github.com/scalacenter/scalajs-bundler/blob/main/sbt-scalajs-bundler/src/sbt-test/sbt-scalajs-bundler/static/).
5757

58-
### Yarn {#yarn}
58+
### Package managers
5959

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

63-
~~~ scala
64-
useYarn := true
65-
~~~
63+
``` scala
64+
jsPackageManager := Yarn()
65+
```
66+
67+
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.
68+
You should check the lockfile into source control.
6669

67-
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.
70+
Package manager behavior can be customized by passing your own [PackageManager](api:scalajsbundler.PackageManager) to the key.
71+
You can use it to modify commands and their arguments for `npm` or `yarn`, or to set up new package managers like
72+
[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/)).
6873

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

7177
### Bundling Mode {#bundling-mode}
7278

sbt-scalajs-bundler/src/main/scala/scalajsbundler/ExternalCommand.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import scalajsbundler.util.Commands
1010
*
1111
* @param name Name of the command to run
1212
*/
13+
@deprecated("Use jsPackageManager instead.")
1314
class ExternalCommand(name: String) {
1415

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

3334
object Yarn extends ExternalCommand("yarn")
3435

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

@@ -89,6 +91,7 @@ object ExternalCommand {
8991
* @param npmExtraArgs Additional arguments to pass to npm
9092
* @param npmPackages Packages to install (e.g. "webpack", "[email protected]")
9193
*/
94+
@deprecated("Use jsPackageManager instead.")
9295
def addPackages(baseDir: File,
9396
installDir: File,
9497
useYarn: Boolean,
@@ -107,6 +110,7 @@ object ExternalCommand {
107110
}
108111
}
109112

113+
@deprecated("Use jsPackageManager instead.")
110114
def install(baseDir: File,
111115
installDir: File,
112116
useYarn: Boolean,

sbt-scalajs-bundler/src/main/scala/scalajsbundler/PackageJson.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ object PackageJson {
3131
currentConfiguration: Configuration,
3232
webpackVersion: String,
3333
webpackDevServerVersion: String,
34-
webpackCliVersion: String
34+
webpackCliVersion: String,
35+
packageManager: PackageManager
3536
): Unit = {
3637
val npmManifestDependencies = NpmDependencies.collectFromClasspath(fullClasspath)
3738
val dependencies =
@@ -62,7 +63,7 @@ object PackageJson {
6263
val packageJson =
6364
JSON.obj(
6465
(
65-
additionalNpmConfig.toSeq :+
66+
(additionalNpmConfig.toSeq ++ packageManager.packageJsonContents.toSeq) :+
6667
"dependencies" -> JSON.objStr(resolveDependencies(dependencies, npmResolutions, log)) :+
6768
"devDependencies" -> JSON.objStr(resolveDependencies(devDependencies, npmResolutions, log))
6869
): _*
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
package scalajsbundler
2+
3+
import java.io.File
4+
5+
import sbt._
6+
import scalajsbundler.util.Commands
7+
import scalajsbundler.util.JSON
8+
9+
trait PackageManager {
10+
11+
val name: String
12+
13+
/**
14+
* Runs the command `cmd`
15+
* @param args Command arguments
16+
* @param workingDir Working directory of the process
17+
* @param logger Logger
18+
*/
19+
def run(args: String*)(workingDir: File, logger: Logger): Unit
20+
21+
def install(baseDir: File, installDir: File, logger: Logger): Unit
22+
23+
val packageJsonContents: Map[String, JSON]
24+
}
25+
26+
object PackageManager {
27+
28+
abstract class ExternalProcess(
29+
val name: String,
30+
val installCommand: String,
31+
val installArgs: Seq[String]
32+
) extends PackageManager {
33+
34+
def run(args: String*)(workingDir: File, logger: Logger): Unit =
35+
Commands.run(cmd ++: args, workingDir, logger)
36+
37+
private val cmd = sys.props("os.name").toLowerCase match {
38+
case os if os.contains("win") => Seq("cmd", "/c", name)
39+
case _ => Seq(name)
40+
}
41+
42+
def install(baseDir: File, installDir: File, logger: Logger): Unit = {
43+
this match {
44+
case lfs: LockFileSupport =>
45+
lfs.lockFileRead(baseDir, installDir, logger)
46+
case _ =>
47+
()
48+
}
49+
50+
run(installCommand +: installArgs: _*)(installDir, logger)
51+
52+
this match {
53+
case lfs: LockFileSupport =>
54+
lfs.lockFileWrite(baseDir, installDir, logger)
55+
case _ =>
56+
()
57+
}
58+
}
59+
}
60+
61+
trait AddPackagesSupport { this: PackageManager =>
62+
63+
val addPackagesCommand: String
64+
val addPackagesArgs: Seq[String]
65+
66+
/**
67+
* Locally install NPM packages
68+
*
69+
* @param baseDir The (sub-)project directory which contains yarn.lock
70+
* @param installDir The directory in which to install the packages
71+
* @param logger sbt logger
72+
* @param npmPackages Packages to install (e.g. "webpack", "[email protected]")
73+
*/
74+
def addPackages(baseDir: File,
75+
installDir: File,
76+
logger: Logger,
77+
)(npmPackages: String*): Unit = {
78+
this match {
79+
case lfs: LockFileSupport =>
80+
lfs.lockFileRead(baseDir, installDir, logger)
81+
case _ =>
82+
()
83+
}
84+
85+
run(addPackagesCommand +: (addPackagesArgs ++ npmPackages): _*)(installDir, logger)
86+
87+
this match {
88+
case lfs: LockFileSupport =>
89+
lfs.lockFileWrite(baseDir, installDir, logger)
90+
case _ =>
91+
()
92+
}
93+
}
94+
}
95+
96+
trait LockFileSupport {
97+
val lockFileName: String
98+
99+
def lockFileRead(
100+
baseDir: File,
101+
installDir: File,
102+
logger: Logger
103+
): Unit = {
104+
val sourceLockFile = baseDir / lockFileName
105+
val targetLockFile = installDir / lockFileName
106+
107+
if (sourceLockFile.exists()) {
108+
logger.info("Using lockfile " + sourceLockFile)
109+
IO.copyFile(sourceLockFile, targetLockFile)
110+
}
111+
}
112+
113+
def lockFileWrite(
114+
baseDir: File,
115+
installDir: File,
116+
logger: Logger
117+
): Unit = {
118+
val sourceLockFile = baseDir / lockFileName
119+
val targetLockFile = installDir / lockFileName
120+
121+
if (targetLockFile.exists()) {
122+
logger.debug("Wrote lockfile to " + sourceLockFile)
123+
IO.copyFile(targetLockFile, sourceLockFile)
124+
}
125+
}
126+
}
127+
128+
final class Npm private (
129+
override val name: String,
130+
val lockFileName: String,
131+
override val installCommand: String,
132+
override val installArgs: Seq[String],
133+
val addPackagesCommand: String,
134+
val addPackagesArgs: Seq[String],
135+
) extends ExternalProcess(name, installCommand, installArgs)
136+
with LockFileSupport
137+
with AddPackagesSupport {
138+
139+
override val packageJsonContents: Map[String, JSON] = Map.empty
140+
141+
private def this() = {
142+
this(
143+
name = "npm",
144+
lockFileName = "package-lock.json",
145+
installCommand = "install",
146+
installArgs = Seq.empty,
147+
addPackagesCommand = "install",
148+
addPackagesArgs = Seq.empty
149+
)
150+
}
151+
152+
def withName(name: String): Npm = copy(name = name)
153+
154+
def withLockFileName(lockFileName: String): Npm = copy(lockFileName = lockFileName)
155+
156+
def withInstallCommand(installCommand: String): Npm = copy(installCommand = installCommand)
157+
158+
def withInstallArgs(installArgs: Seq[String]): Npm = copy(installArgs = installArgs)
159+
160+
def withAddPackagesCommand(addPackagesCommand: String): Npm = copy(addPackagesCommand = addPackagesCommand)
161+
162+
def withAddPackagesArgs(addPackagesArgs: Seq[String]): Npm = copy(addPackagesArgs = addPackagesArgs)
163+
164+
private def copy(
165+
name: String = name,
166+
lockFileName: String = lockFileName,
167+
installCommand: String = installCommand,
168+
installArgs: Seq[String] = installArgs,
169+
addPackagesCommand: String = addPackagesCommand,
170+
addPackagesArgs: Seq[String] = addPackagesArgs
171+
) = {
172+
new Npm(
173+
name,
174+
lockFileName,
175+
installCommand,
176+
installArgs,
177+
addPackagesCommand,
178+
addPackagesArgs
179+
)
180+
}
181+
}
182+
object Npm {
183+
def apply() = new Npm()
184+
}
185+
186+
final class Yarn private (
187+
override val name: String,
188+
val version: Option[String],
189+
val lockFileName: String,
190+
override val installCommand: String,
191+
override val installArgs: Seq[String],
192+
val addPackagesCommand: String,
193+
val addPackagesArgs: Seq[String],
194+
) extends ExternalProcess(name, installCommand, installArgs)
195+
with LockFileSupport
196+
with AddPackagesSupport {
197+
198+
override val packageJsonContents: Map[String, JSON] =
199+
version.map(v => Map("packageManager" -> JSON.str(s"$name@$v"))).getOrElse(Map.empty)
200+
201+
private def this() = {
202+
this(
203+
name = "yarn",
204+
version = None,
205+
lockFileName = "yarn.lock",
206+
installCommand = "install",
207+
installArgs = Seq.empty,
208+
addPackagesCommand = "add",
209+
addPackagesArgs = Seq.empty
210+
)
211+
}
212+
213+
def withName(name: String): Yarn = copy(name = name)
214+
215+
def withVersion(version: Option[String]): Yarn = copy(version = version)
216+
217+
def withLockFileName(lockFileName: String): Yarn = copy(lockFileName = lockFileName)
218+
219+
def withInstallCommand(installCommand: String): Yarn = copy(installCommand = installCommand)
220+
221+
def withInstallArgs(installArgs: Seq[String]): Yarn = copy(installArgs = installArgs)
222+
223+
def withAddPackagesCommand(addPackagesCommand: String): Yarn = copy(addPackagesCommand = addPackagesCommand)
224+
225+
def withAddPackagesArgs(addPackagesArgs: Seq[String]): Yarn = copy(addPackagesArgs = addPackagesArgs)
226+
227+
private def copy(
228+
name: String = name,
229+
version: Option[String] = version,
230+
lockFileName: String = lockFileName,
231+
installCommand: String = installCommand,
232+
installArgs: Seq[String] = installArgs,
233+
addPackagesCommand: String = addPackagesCommand,
234+
addPackagesArgs: Seq[String] = addPackagesArgs
235+
) = {
236+
new Yarn(
237+
name,
238+
version,
239+
lockFileName,
240+
installCommand,
241+
installArgs,
242+
addPackagesCommand,
243+
addPackagesArgs
244+
)
245+
}
246+
}
247+
object Yarn {
248+
val DefaultArgs: Seq[String] = Seq("--non-interactive", "--mutex", "network")
249+
250+
def apply() = new Yarn()
251+
}
252+
}

0 commit comments

Comments
 (0)