Skip to content

Commit 16ecc0f

Browse files
authored
Bun support (node-gradle#290)
Add support for Bun
1 parent 24ba5dd commit 16ecc0f

23 files changed

+1504
-2
lines changed

build.gradle.kts

+2-2
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ gradlePlugin {
167167
id = "com.github.node-gradle.node"
168168
implementationClass = "com.github.gradle.node.NodePlugin"
169169
displayName = "Gradle Node.js Plugin"
170-
description = "Gradle plugin for executing Node.js scripts. Supports npm, pnpm and Yarn."
170+
description = "Gradle plugin for executing Node.js scripts. Supports npm, pnpm, Yarn and Bun."
171171
}
172172
}
173173
}
@@ -176,7 +176,7 @@ pluginBundle {
176176
website = "https://github.com/node-gradle/gradle-node-plugin"
177177
vcsUrl = "https://github.com/node-gradle/gradle-node-plugin"
178178

179-
tags = listOf("java", "node", "node.js", "npm", "yarn", "pnpm")
179+
tags = listOf("java", "node", "node.js", "npm", "yarn", "pnpm", "bun")
180180
}
181181

182182
tasks.wrapper {

src/main/kotlin/com/github/gradle/node/NodeExtension.kt

+14
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ open class NodeExtension(project: Project) {
3030
*/
3131
val yarnWorkDir = project.objects.directoryProperty().convention(cacheDir.dir("yarn"))
3232

33+
/**
34+
* The directory where Bun is installed (when a Bun task is used)
35+
*/
36+
val bunWorkDir = project.objects.directoryProperty().convention(cacheDir.dir("bun"))
37+
3338
/**
3439
* The Node.js project directory location
3540
* This is where the package.json file and node_modules directory are located
@@ -64,6 +69,13 @@ open class NodeExtension(project: Project) {
6469
*/
6570
val yarnVersion = project.objects.property<String>().convention("")
6671

72+
/**
73+
* Version of Bun to use
74+
* Any Bun task first installs Bun in the bunWorkDir
75+
* It uses the specified version if defined and the latest version otherwise (by default)
76+
*/
77+
val bunVersion = project.objects.property<String>().convention("")
78+
6779
/**
6880
* Base URL for fetching node distributions
6981
* Only used if download is true
@@ -84,6 +96,8 @@ open class NodeExtension(project: Project) {
8496
val npxCommand = project.objects.property<String>().convention("npx")
8597
val pnpmCommand = project.objects.property<String>().convention("pnpm")
8698
val yarnCommand = project.objects.property<String>().convention("yarn")
99+
val bunCommand = project.objects.property<String>().convention("bun")
100+
val bunxCommand = project.objects.property<String>().convention("bunx")
87101

88102
/**
89103
* The npm command executed by the npmInstall task

src/main/kotlin/com/github/gradle/node/NodePlugin.kt

+9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package com.github.gradle.node
22

3+
import com.github.gradle.node.bun.task.BunInstallTask
4+
import com.github.gradle.node.bun.task.BunSetupTask
5+
import com.github.gradle.node.bun.task.BunTask
6+
import com.github.gradle.node.bun.task.BunxTask
37
import com.github.gradle.node.npm.proxy.ProxySettings
48
import com.github.gradle.node.npm.task.NpmInstallTask
59
import com.github.gradle.node.npm.task.NpmSetupTask
@@ -94,6 +98,8 @@ class NodePlugin : Plugin<Project> {
9498
addGlobalType<NpxTask>()
9599
addGlobalType<PnpmTask>()
96100
addGlobalType<YarnTask>()
101+
addGlobalType<BunTask>()
102+
addGlobalType<BunxTask>()
97103
addGlobalType<ProxySettings>()
98104
}
99105

@@ -105,10 +111,12 @@ class NodePlugin : Plugin<Project> {
105111
project.tasks.register<NpmInstallTask>(NpmInstallTask.NAME)
106112
project.tasks.register<PnpmInstallTask>(PnpmInstallTask.NAME)
107113
project.tasks.register<YarnInstallTask>(YarnInstallTask.NAME)
114+
project.tasks.register<BunInstallTask>(BunInstallTask.NAME)
108115
project.tasks.register<NodeSetupTask>(NodeSetupTask.NAME)
109116
project.tasks.register<NpmSetupTask>(NpmSetupTask.NAME)
110117
project.tasks.register<PnpmSetupTask>(PnpmSetupTask.NAME)
111118
project.tasks.register<YarnSetupTask>(YarnSetupTask.NAME)
119+
project.tasks.register<BunSetupTask>(BunSetupTask.NAME)
112120
}
113121

114122
private fun addNpmRule(enableTaskRules: Property<Boolean>) { // note this rule also makes it possible to specify e.g. "dependsOn npm_install"
@@ -197,5 +205,6 @@ class NodePlugin : Plugin<Project> {
197205
const val NPM_GROUP = "npm"
198206
const val PNPM_GROUP = "pnpm"
199207
const val YARN_GROUP = "Yarn"
208+
const val BUN_GROUP = "Bun"
200209
}
201210
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package com.github.gradle.node.bun.exec
2+
3+
import com.github.gradle.node.NodeExtension
4+
import com.github.gradle.node.exec.ExecConfiguration
5+
import com.github.gradle.node.exec.ExecRunner
6+
import com.github.gradle.node.exec.NodeExecConfiguration
7+
import com.github.gradle.node.npm.exec.NpmExecConfiguration
8+
import com.github.gradle.node.npm.proxy.NpmProxy
9+
import com.github.gradle.node.util.ProjectApiHelper
10+
import com.github.gradle.node.util.zip
11+
import com.github.gradle.node.variant.VariantComputer
12+
import com.github.gradle.node.variant.computeNodeExec
13+
import org.gradle.api.provider.Provider
14+
import org.gradle.api.provider.ProviderFactory
15+
import org.gradle.process.ExecResult
16+
import javax.inject.Inject
17+
18+
abstract class BunExecRunner {
19+
@get:Inject
20+
abstract val providers: ProviderFactory
21+
22+
fun executeBunCommand(project: ProjectApiHelper, extension: NodeExtension, nodeExecConfiguration: NodeExecConfiguration, variants: VariantComputer): ExecResult {
23+
val bunExecConfiguration = NpmExecConfiguration("bun"
24+
) { variantComputer, nodeExtension, binDir -> variantComputer.computeBunExec(nodeExtension, binDir) }
25+
26+
val enhancedNodeExecConfiguration = NpmProxy.addProxyEnvironmentVariables(extension.nodeProxySettings.get(), nodeExecConfiguration)
27+
val execConfiguration = computeExecConfiguration(extension, bunExecConfiguration, enhancedNodeExecConfiguration, variants).get()
28+
return ExecRunner().execute(project, extension, execConfiguration)
29+
}
30+
31+
fun executeBunxCommand(project: ProjectApiHelper, extension: NodeExtension, nodeExecConfiguration: NodeExecConfiguration, variants: VariantComputer): ExecResult {
32+
val bunExecConfiguration = NpmExecConfiguration("bunx") { variantComputer, nodeExtension, bunBinDir ->
33+
variantComputer.computeBunxExec(nodeExtension, bunBinDir)
34+
}
35+
36+
val enhancedNodeExecConfiguration = NpmProxy.addProxyEnvironmentVariables(extension.nodeProxySettings.get(), nodeExecConfiguration)
37+
val execConfiguration = computeExecConfiguration(extension, bunExecConfiguration, enhancedNodeExecConfiguration, variants).get()
38+
return ExecRunner().execute(project, extension, execConfiguration)
39+
}
40+
41+
private fun computeExecConfiguration(extension: NodeExtension, bunExecConfiguration: NpmExecConfiguration,
42+
nodeExecConfiguration: NodeExecConfiguration,
43+
variantComputer: VariantComputer): Provider<ExecConfiguration> {
44+
val additionalBinPathProvider = computeAdditionalBinPath(extension, variantComputer)
45+
val executableAndScriptProvider = computeExecutable(extension, bunExecConfiguration, variantComputer)
46+
return zip(additionalBinPathProvider, executableAndScriptProvider)
47+
.map { (additionalBinPath, executableAndScript) ->
48+
val argsPrefix =
49+
if (executableAndScript.script != null) listOf(executableAndScript.script) else listOf()
50+
val args = argsPrefix.plus(nodeExecConfiguration.command)
51+
ExecConfiguration(executableAndScript.executable, args, additionalBinPath,
52+
nodeExecConfiguration.environment, nodeExecConfiguration.workingDir,
53+
nodeExecConfiguration.ignoreExitValue, nodeExecConfiguration.execOverrides)
54+
}
55+
}
56+
57+
private fun computeExecutable(
58+
nodeExtension: NodeExtension,
59+
bunExecConfiguration: NpmExecConfiguration,
60+
variantComputer: VariantComputer
61+
):
62+
Provider<ExecutableAndScript> {
63+
val nodeDirProvider = nodeExtension.resolvedNodeDir
64+
val bunDirProvider = variantComputer.computeBunDir(nodeExtension)
65+
val nodeBinDirProvider = variantComputer.computeNodeBinDir(nodeDirProvider, nodeExtension.resolvedPlatform)
66+
val bunBinDirProvider = variantComputer.computeBunBinDir(bunDirProvider, nodeExtension.resolvedPlatform)
67+
val nodeExecProvider = computeNodeExec(nodeExtension, nodeBinDirProvider)
68+
val executableProvider =
69+
bunExecConfiguration.commandExecComputer(variantComputer, nodeExtension, bunBinDirProvider)
70+
71+
return zip(nodeExtension.download, nodeExtension.nodeProjectDir, executableProvider, nodeExecProvider).map {
72+
val (download, nodeProjectDir, executable, nodeExec) = it
73+
if (download) {
74+
val localCommandScript = nodeProjectDir.dir("node_modules/bun/bin")
75+
.file("${bunExecConfiguration.command}.js").asFile
76+
if (localCommandScript.exists()) {
77+
return@map ExecutableAndScript(nodeExec, localCommandScript.absolutePath)
78+
}
79+
}
80+
return@map ExecutableAndScript(executable)
81+
}
82+
}
83+
84+
private data class ExecutableAndScript(
85+
val executable: String,
86+
val script: String? = null
87+
)
88+
89+
private fun computeAdditionalBinPath(nodeExtension: NodeExtension, variantComputer: VariantComputer): Provider<List<String>> {
90+
return nodeExtension.download.flatMap { download ->
91+
if (!download) {
92+
providers.provider { listOf<String>() }
93+
}
94+
val bunDirProvider = variantComputer.computeBunDir(nodeExtension)
95+
val bunBinDirProvider = variantComputer.computeBunBinDir(bunDirProvider, nodeExtension.resolvedPlatform)
96+
bunBinDirProvider.map { file -> listOf(file.asFile.absolutePath) }
97+
}
98+
}
99+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.github.gradle.node.bun.task
2+
3+
import com.github.gradle.node.NodeExtension
4+
import com.github.gradle.node.NodePlugin
5+
import com.github.gradle.node.task.BaseTask
6+
import com.github.gradle.node.util.DefaultProjectApiHelper
7+
import org.gradle.api.Action
8+
import org.gradle.api.model.ObjectFactory
9+
import org.gradle.api.provider.ProviderFactory
10+
import org.gradle.api.tasks.Input
11+
import org.gradle.api.tasks.Internal
12+
import org.gradle.api.tasks.Optional
13+
import org.gradle.kotlin.dsl.listProperty
14+
import org.gradle.kotlin.dsl.mapProperty
15+
import org.gradle.kotlin.dsl.newInstance
16+
import org.gradle.kotlin.dsl.property
17+
import org.gradle.process.ExecSpec
18+
import javax.inject.Inject
19+
20+
abstract class BunAbstractTask : BaseTask() {
21+
@get:Inject
22+
abstract val objects: ObjectFactory
23+
24+
@get:Inject
25+
abstract val providers: ProviderFactory
26+
27+
@get:Optional
28+
@get:Input
29+
val args = objects.listProperty<String>()
30+
31+
@get:Input
32+
val ignoreExitValue = objects.property<Boolean>().convention(false)
33+
34+
@get:Input
35+
val environment = objects.mapProperty<String, String>()
36+
37+
@get:Internal
38+
val workingDir = objects.directoryProperty()
39+
40+
@get:Internal
41+
val execOverrides = objects.property<Action<ExecSpec>>()
42+
43+
@get:Internal
44+
val projectHelper = project.objects.newInstance<DefaultProjectApiHelper>()
45+
46+
@get:Internal
47+
val nodeExtension = NodeExtension[project]
48+
49+
init {
50+
group = NodePlugin.BUN_GROUP
51+
dependsOn(BunSetupTask.NAME)
52+
}
53+
54+
// For DSL
55+
@Suppress("unused")
56+
fun execOverrides(execOverrides: Action<ExecSpec>) {
57+
this.execOverrides.set(execOverrides)
58+
}
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package com.github.gradle.node.bun.task
2+
3+
import com.github.gradle.node.NodePlugin
4+
import com.github.gradle.node.util.zip
5+
import org.gradle.api.Action
6+
import org.gradle.api.file.ConfigurableFileTree
7+
import org.gradle.api.file.Directory
8+
import org.gradle.api.file.FileTree
9+
import org.gradle.api.provider.Provider
10+
import org.gradle.api.tasks.*
11+
import org.gradle.api.tasks.PathSensitivity.RELATIVE
12+
import org.gradle.kotlin.dsl.property
13+
import java.io.File
14+
15+
/**
16+
* bun install that only gets executed if gradle decides so.
17+
*/
18+
abstract class BunInstallTask : BunTask() {
19+
20+
@get:Internal
21+
val nodeModulesOutputFilter =
22+
objects.property<Action<ConfigurableFileTree>>()
23+
24+
25+
init {
26+
group = NodePlugin.BUN_GROUP
27+
description = "Install packages from package.json."
28+
dependsOn(BunSetupTask.NAME)
29+
bunCommand.set(nodeExtension.npmInstallCommand.map {
30+
when(it) {
31+
"ci" -> listOf("install", "--frozen-lockfile")
32+
else -> listOf(it)
33+
}
34+
})
35+
}
36+
37+
@PathSensitive(RELATIVE)
38+
@InputFile
39+
protected fun getPackageJsonFile(): File? {
40+
return projectFileIfExists("package.json").orNull
41+
}
42+
43+
@Optional
44+
@OutputFile
45+
protected fun getBunLockAsOutput(): File? {
46+
return projectFileIfExists("bun.lockb").orNull
47+
}
48+
49+
private fun projectFileIfExists(name: String): Provider<File?> {
50+
return nodeExtension.nodeProjectDir.map { it.file(name).asFile }
51+
.flatMap { if (it.exists()) providers.provider { it } else providers.provider { null } }
52+
}
53+
54+
@Optional
55+
@OutputDirectory
56+
@Suppress("unused")
57+
protected fun getNodeModulesDirectory(): Provider<Directory> {
58+
val filter = nodeModulesOutputFilter.orNull
59+
return if (filter == null) nodeExtension.nodeProjectDir.dir("node_modules")
60+
else providers.provider { null }
61+
}
62+
63+
@Optional
64+
@OutputFiles
65+
@Suppress("unused")
66+
protected fun getNodeModulesFiles(): Provider<FileTree> {
67+
val nodeModulesDirectoryProvider = nodeExtension.nodeProjectDir.dir("node_modules")
68+
return zip(nodeModulesDirectoryProvider, nodeModulesOutputFilter)
69+
.flatMap { (nodeModulesDirectory, nodeModulesOutputFilter) ->
70+
if (nodeModulesOutputFilter != null) {
71+
val fileTree = projectHelper.fileTree(nodeModulesDirectory)
72+
nodeModulesOutputFilter.execute(fileTree)
73+
providers.provider { fileTree }
74+
} else providers.provider { null }
75+
}
76+
}
77+
78+
// For DSL
79+
@Suppress("unused")
80+
fun nodeModulesOutputFilter(nodeModulesOutputFilter: Action<ConfigurableFileTree>) {
81+
this.nodeModulesOutputFilter.set(nodeModulesOutputFilter)
82+
}
83+
84+
companion object {
85+
const val NAME = "bunInstall"
86+
}
87+
88+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.github.gradle.node.bun.task
2+
3+
import com.github.gradle.node.NodePlugin
4+
import com.github.gradle.node.npm.task.NpmSetupTask
5+
import com.github.gradle.node.variant.VariantComputer
6+
import org.gradle.api.provider.Provider
7+
import org.gradle.api.tasks.Input
8+
import org.gradle.api.tasks.OutputDirectory
9+
10+
/**
11+
* bun install that only gets executed if gradle decides so.
12+
*/
13+
abstract class BunSetupTask : NpmSetupTask() {
14+
15+
init {
16+
group = NodePlugin.BUN_GROUP
17+
description = "Setup a specific version of Bun to be used by the build."
18+
}
19+
20+
@Input
21+
override fun getVersion(): Provider<String> {
22+
return nodeExtension.bunVersion
23+
}
24+
25+
@get:OutputDirectory
26+
val bunDir by lazy {
27+
val variantComputer = VariantComputer()
28+
variantComputer.computeBunDir(nodeExtension)
29+
}
30+
31+
override fun computeCommand(): List<String> {
32+
val version = nodeExtension.bunVersion.get()
33+
val bunDir = bunDir.get()
34+
val bunPackage = if (version.isNotBlank()) "bun@$version" else "bun"
35+
return listOf(
36+
"install",
37+
"--global",
38+
"--no-save",
39+
"--prefix",
40+
bunDir.asFile.absolutePath,
41+
bunPackage
42+
) + args.get()
43+
}
44+
45+
override fun isTaskEnabled(): Boolean {
46+
return true
47+
}
48+
49+
companion object {
50+
const val NAME = "bunSetup"
51+
}
52+
53+
}

0 commit comments

Comments
 (0)