Skip to content
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

Bun support #290

Merged
merged 13 commits into from
Oct 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading