Skip to content

Commit

Permalink
Bun support (#290)
Browse files Browse the repository at this point in the history
Add support for Bun
  • Loading branch information
harrel56 authored Oct 22, 2023
1 parent 24ba5dd commit 16ecc0f
Show file tree
Hide file tree
Showing 23 changed files with 1,504 additions and 2 deletions.
4 changes: 2 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ gradlePlugin {
id = "com.github.node-gradle.node"
implementationClass = "com.github.gradle.node.NodePlugin"
displayName = "Gradle Node.js Plugin"
description = "Gradle plugin for executing Node.js scripts. Supports npm, pnpm and Yarn."
description = "Gradle plugin for executing Node.js scripts. Supports npm, pnpm, Yarn and Bun."
}
}
}
Expand All @@ -176,7 +176,7 @@ pluginBundle {
website = "https://github.com/node-gradle/gradle-node-plugin"
vcsUrl = "https://github.com/node-gradle/gradle-node-plugin"

tags = listOf("java", "node", "node.js", "npm", "yarn", "pnpm")
tags = listOf("java", "node", "node.js", "npm", "yarn", "pnpm", "bun")
}

tasks.wrapper {
Expand Down
14 changes: 14 additions & 0 deletions src/main/kotlin/com/github/gradle/node/NodeExtension.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ open class NodeExtension(project: Project) {
*/
val yarnWorkDir = project.objects.directoryProperty().convention(cacheDir.dir("yarn"))

/**
* The directory where Bun is installed (when a Bun task is used)
*/
val bunWorkDir = project.objects.directoryProperty().convention(cacheDir.dir("bun"))

/**
* The Node.js project directory location
* This is where the package.json file and node_modules directory are located
Expand Down Expand Up @@ -64,6 +69,13 @@ open class NodeExtension(project: Project) {
*/
val yarnVersion = project.objects.property<String>().convention("")

/**
* Version of Bun to use
* Any Bun task first installs Bun in the bunWorkDir
* It uses the specified version if defined and the latest version otherwise (by default)
*/
val bunVersion = project.objects.property<String>().convention("")

/**
* Base URL for fetching node distributions
* Only used if download is true
Expand All @@ -84,6 +96,8 @@ open class NodeExtension(project: Project) {
val npxCommand = project.objects.property<String>().convention("npx")
val pnpmCommand = project.objects.property<String>().convention("pnpm")
val yarnCommand = project.objects.property<String>().convention("yarn")
val bunCommand = project.objects.property<String>().convention("bun")
val bunxCommand = project.objects.property<String>().convention("bunx")

/**
* The npm command executed by the npmInstall task
Expand Down
9 changes: 9 additions & 0 deletions src/main/kotlin/com/github/gradle/node/NodePlugin.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package com.github.gradle.node

import com.github.gradle.node.bun.task.BunInstallTask
import com.github.gradle.node.bun.task.BunSetupTask
import com.github.gradle.node.bun.task.BunTask
import com.github.gradle.node.bun.task.BunxTask
import com.github.gradle.node.npm.proxy.ProxySettings
import com.github.gradle.node.npm.task.NpmInstallTask
import com.github.gradle.node.npm.task.NpmSetupTask
Expand Down Expand Up @@ -94,6 +98,8 @@ class NodePlugin : Plugin<Project> {
addGlobalType<NpxTask>()
addGlobalType<PnpmTask>()
addGlobalType<YarnTask>()
addGlobalType<BunTask>()
addGlobalType<BunxTask>()
addGlobalType<ProxySettings>()
}

Expand All @@ -105,10 +111,12 @@ class NodePlugin : Plugin<Project> {
project.tasks.register<NpmInstallTask>(NpmInstallTask.NAME)
project.tasks.register<PnpmInstallTask>(PnpmInstallTask.NAME)
project.tasks.register<YarnInstallTask>(YarnInstallTask.NAME)
project.tasks.register<BunInstallTask>(BunInstallTask.NAME)
project.tasks.register<NodeSetupTask>(NodeSetupTask.NAME)
project.tasks.register<NpmSetupTask>(NpmSetupTask.NAME)
project.tasks.register<PnpmSetupTask>(PnpmSetupTask.NAME)
project.tasks.register<YarnSetupTask>(YarnSetupTask.NAME)
project.tasks.register<BunSetupTask>(BunSetupTask.NAME)
}

private fun addNpmRule(enableTaskRules: Property<Boolean>) { // note this rule also makes it possible to specify e.g. "dependsOn npm_install"
Expand Down Expand Up @@ -197,5 +205,6 @@ class NodePlugin : Plugin<Project> {
const val NPM_GROUP = "npm"
const val PNPM_GROUP = "pnpm"
const val YARN_GROUP = "Yarn"
const val BUN_GROUP = "Bun"
}
}
99 changes: 99 additions & 0 deletions src/main/kotlin/com/github/gradle/node/bun/exec/BunExecRunner.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.github.gradle.node.bun.exec

import com.github.gradle.node.NodeExtension
import com.github.gradle.node.exec.ExecConfiguration
import com.github.gradle.node.exec.ExecRunner
import com.github.gradle.node.exec.NodeExecConfiguration
import com.github.gradle.node.npm.exec.NpmExecConfiguration
import com.github.gradle.node.npm.proxy.NpmProxy
import com.github.gradle.node.util.ProjectApiHelper
import com.github.gradle.node.util.zip
import com.github.gradle.node.variant.VariantComputer
import com.github.gradle.node.variant.computeNodeExec
import org.gradle.api.provider.Provider
import org.gradle.api.provider.ProviderFactory
import org.gradle.process.ExecResult
import javax.inject.Inject

abstract class BunExecRunner {
@get:Inject
abstract val providers: ProviderFactory

fun executeBunCommand(project: ProjectApiHelper, extension: NodeExtension, nodeExecConfiguration: NodeExecConfiguration, variants: VariantComputer): ExecResult {
val bunExecConfiguration = NpmExecConfiguration("bun"
) { variantComputer, nodeExtension, binDir -> variantComputer.computeBunExec(nodeExtension, binDir) }

val enhancedNodeExecConfiguration = NpmProxy.addProxyEnvironmentVariables(extension.nodeProxySettings.get(), nodeExecConfiguration)
val execConfiguration = computeExecConfiguration(extension, bunExecConfiguration, enhancedNodeExecConfiguration, variants).get()
return ExecRunner().execute(project, extension, execConfiguration)
}

fun executeBunxCommand(project: ProjectApiHelper, extension: NodeExtension, nodeExecConfiguration: NodeExecConfiguration, variants: VariantComputer): ExecResult {
val bunExecConfiguration = NpmExecConfiguration("bunx") { variantComputer, nodeExtension, bunBinDir ->
variantComputer.computeBunxExec(nodeExtension, bunBinDir)
}

val enhancedNodeExecConfiguration = NpmProxy.addProxyEnvironmentVariables(extension.nodeProxySettings.get(), nodeExecConfiguration)
val execConfiguration = computeExecConfiguration(extension, bunExecConfiguration, enhancedNodeExecConfiguration, variants).get()
return ExecRunner().execute(project, extension, execConfiguration)
}

private fun computeExecConfiguration(extension: NodeExtension, bunExecConfiguration: NpmExecConfiguration,
nodeExecConfiguration: NodeExecConfiguration,
variantComputer: VariantComputer): Provider<ExecConfiguration> {
val additionalBinPathProvider = computeAdditionalBinPath(extension, variantComputer)
val executableAndScriptProvider = computeExecutable(extension, bunExecConfiguration, variantComputer)
return zip(additionalBinPathProvider, executableAndScriptProvider)
.map { (additionalBinPath, executableAndScript) ->
val argsPrefix =
if (executableAndScript.script != null) listOf(executableAndScript.script) else listOf()
val args = argsPrefix.plus(nodeExecConfiguration.command)
ExecConfiguration(executableAndScript.executable, args, additionalBinPath,
nodeExecConfiguration.environment, nodeExecConfiguration.workingDir,
nodeExecConfiguration.ignoreExitValue, nodeExecConfiguration.execOverrides)
}
}

private fun computeExecutable(
nodeExtension: NodeExtension,
bunExecConfiguration: NpmExecConfiguration,
variantComputer: VariantComputer
):
Provider<ExecutableAndScript> {
val nodeDirProvider = nodeExtension.resolvedNodeDir
val bunDirProvider = variantComputer.computeBunDir(nodeExtension)
val nodeBinDirProvider = variantComputer.computeNodeBinDir(nodeDirProvider, nodeExtension.resolvedPlatform)
val bunBinDirProvider = variantComputer.computeBunBinDir(bunDirProvider, nodeExtension.resolvedPlatform)
val nodeExecProvider = computeNodeExec(nodeExtension, nodeBinDirProvider)
val executableProvider =
bunExecConfiguration.commandExecComputer(variantComputer, nodeExtension, bunBinDirProvider)

return zip(nodeExtension.download, nodeExtension.nodeProjectDir, executableProvider, nodeExecProvider).map {
val (download, nodeProjectDir, executable, nodeExec) = it
if (download) {
val localCommandScript = nodeProjectDir.dir("node_modules/bun/bin")
.file("${bunExecConfiguration.command}.js").asFile
if (localCommandScript.exists()) {
return@map ExecutableAndScript(nodeExec, localCommandScript.absolutePath)
}
}
return@map ExecutableAndScript(executable)
}
}

private data class ExecutableAndScript(
val executable: String,
val script: String? = null
)

private fun computeAdditionalBinPath(nodeExtension: NodeExtension, variantComputer: VariantComputer): Provider<List<String>> {
return nodeExtension.download.flatMap { download ->
if (!download) {
providers.provider { listOf<String>() }
}
val bunDirProvider = variantComputer.computeBunDir(nodeExtension)
val bunBinDirProvider = variantComputer.computeBunBinDir(bunDirProvider, nodeExtension.resolvedPlatform)
bunBinDirProvider.map { file -> listOf(file.asFile.absolutePath) }
}
}
}
59 changes: 59 additions & 0 deletions src/main/kotlin/com/github/gradle/node/bun/task/BunAbstractTask.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.github.gradle.node.bun.task

import com.github.gradle.node.NodeExtension
import com.github.gradle.node.NodePlugin
import com.github.gradle.node.task.BaseTask
import com.github.gradle.node.util.DefaultProjectApiHelper
import org.gradle.api.Action
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.ProviderFactory
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Optional
import org.gradle.kotlin.dsl.listProperty
import org.gradle.kotlin.dsl.mapProperty
import org.gradle.kotlin.dsl.newInstance
import org.gradle.kotlin.dsl.property
import org.gradle.process.ExecSpec
import javax.inject.Inject

abstract class BunAbstractTask : BaseTask() {
@get:Inject
abstract val objects: ObjectFactory

@get:Inject
abstract val providers: ProviderFactory

@get:Optional
@get:Input
val args = objects.listProperty<String>()

@get:Input
val ignoreExitValue = objects.property<Boolean>().convention(false)

@get:Input
val environment = objects.mapProperty<String, String>()

@get:Internal
val workingDir = objects.directoryProperty()

@get:Internal
val execOverrides = objects.property<Action<ExecSpec>>()

@get:Internal
val projectHelper = project.objects.newInstance<DefaultProjectApiHelper>()

@get:Internal
val nodeExtension = NodeExtension[project]

init {
group = NodePlugin.BUN_GROUP
dependsOn(BunSetupTask.NAME)
}

// For DSL
@Suppress("unused")
fun execOverrides(execOverrides: Action<ExecSpec>) {
this.execOverrides.set(execOverrides)
}
}
88 changes: 88 additions & 0 deletions src/main/kotlin/com/github/gradle/node/bun/task/BunInstallTask.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.github.gradle.node.bun.task

import com.github.gradle.node.NodePlugin
import com.github.gradle.node.util.zip
import org.gradle.api.Action
import org.gradle.api.file.ConfigurableFileTree
import org.gradle.api.file.Directory
import org.gradle.api.file.FileTree
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.*
import org.gradle.api.tasks.PathSensitivity.RELATIVE
import org.gradle.kotlin.dsl.property
import java.io.File

/**
* bun install that only gets executed if gradle decides so.
*/
abstract class BunInstallTask : BunTask() {

@get:Internal
val nodeModulesOutputFilter =
objects.property<Action<ConfigurableFileTree>>()


init {
group = NodePlugin.BUN_GROUP
description = "Install packages from package.json."
dependsOn(BunSetupTask.NAME)
bunCommand.set(nodeExtension.npmInstallCommand.map {
when(it) {
"ci" -> listOf("install", "--frozen-lockfile")
else -> listOf(it)
}
})
}

@PathSensitive(RELATIVE)
@InputFile
protected fun getPackageJsonFile(): File? {
return projectFileIfExists("package.json").orNull
}

@Optional
@OutputFile
protected fun getBunLockAsOutput(): File? {
return projectFileIfExists("bun.lockb").orNull
}

private fun projectFileIfExists(name: String): Provider<File?> {
return nodeExtension.nodeProjectDir.map { it.file(name).asFile }
.flatMap { if (it.exists()) providers.provider { it } else providers.provider { null } }
}

@Optional
@OutputDirectory
@Suppress("unused")
protected fun getNodeModulesDirectory(): Provider<Directory> {
val filter = nodeModulesOutputFilter.orNull
return if (filter == null) nodeExtension.nodeProjectDir.dir("node_modules")
else providers.provider { null }
}

@Optional
@OutputFiles
@Suppress("unused")
protected fun getNodeModulesFiles(): Provider<FileTree> {
val nodeModulesDirectoryProvider = nodeExtension.nodeProjectDir.dir("node_modules")
return zip(nodeModulesDirectoryProvider, nodeModulesOutputFilter)
.flatMap { (nodeModulesDirectory, nodeModulesOutputFilter) ->
if (nodeModulesOutputFilter != null) {
val fileTree = projectHelper.fileTree(nodeModulesDirectory)
nodeModulesOutputFilter.execute(fileTree)
providers.provider { fileTree }
} else providers.provider { null }
}
}

// For DSL
@Suppress("unused")
fun nodeModulesOutputFilter(nodeModulesOutputFilter: Action<ConfigurableFileTree>) {
this.nodeModulesOutputFilter.set(nodeModulesOutputFilter)
}

companion object {
const val NAME = "bunInstall"
}

}
53 changes: 53 additions & 0 deletions src/main/kotlin/com/github/gradle/node/bun/task/BunSetupTask.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.github.gradle.node.bun.task

import com.github.gradle.node.NodePlugin
import com.github.gradle.node.npm.task.NpmSetupTask
import com.github.gradle.node.variant.VariantComputer
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputDirectory

/**
* bun install that only gets executed if gradle decides so.
*/
abstract class BunSetupTask : NpmSetupTask() {

init {
group = NodePlugin.BUN_GROUP
description = "Setup a specific version of Bun to be used by the build."
}

@Input
override fun getVersion(): Provider<String> {
return nodeExtension.bunVersion
}

@get:OutputDirectory
val bunDir by lazy {
val variantComputer = VariantComputer()
variantComputer.computeBunDir(nodeExtension)
}

override fun computeCommand(): List<String> {
val version = nodeExtension.bunVersion.get()
val bunDir = bunDir.get()
val bunPackage = if (version.isNotBlank()) "bun@$version" else "bun"
return listOf(
"install",
"--global",
"--no-save",
"--prefix",
bunDir.asFile.absolutePath,
bunPackage
) + args.get()
}

override fun isTaskEnabled(): Boolean {
return true
}

companion object {
const val NAME = "bunSetup"
}

}
Loading

0 comments on commit 16ecc0f

Please sign in to comment.