diff --git a/build.gradle.kts b/build.gradle.kts index 807c779e..7869340c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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." } } } @@ -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 { diff --git a/src/main/kotlin/com/github/gradle/node/NodeExtension.kt b/src/main/kotlin/com/github/gradle/node/NodeExtension.kt index dc00583c..8fa9a68c 100644 --- a/src/main/kotlin/com/github/gradle/node/NodeExtension.kt +++ b/src/main/kotlin/com/github/gradle/node/NodeExtension.kt @@ -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 @@ -64,6 +69,13 @@ open class NodeExtension(project: Project) { */ val yarnVersion = project.objects.property().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().convention("") + /** * Base URL for fetching node distributions * Only used if download is true @@ -84,6 +96,8 @@ open class NodeExtension(project: Project) { val npxCommand = project.objects.property().convention("npx") val pnpmCommand = project.objects.property().convention("pnpm") val yarnCommand = project.objects.property().convention("yarn") + val bunCommand = project.objects.property().convention("bun") + val bunxCommand = project.objects.property().convention("bunx") /** * The npm command executed by the npmInstall task diff --git a/src/main/kotlin/com/github/gradle/node/NodePlugin.kt b/src/main/kotlin/com/github/gradle/node/NodePlugin.kt index d9a7855c..846b8e20 100644 --- a/src/main/kotlin/com/github/gradle/node/NodePlugin.kt +++ b/src/main/kotlin/com/github/gradle/node/NodePlugin.kt @@ -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 @@ -94,6 +98,8 @@ class NodePlugin : Plugin { addGlobalType() addGlobalType() addGlobalType() + addGlobalType() + addGlobalType() addGlobalType() } @@ -105,10 +111,12 @@ class NodePlugin : Plugin { project.tasks.register(NpmInstallTask.NAME) project.tasks.register(PnpmInstallTask.NAME) project.tasks.register(YarnInstallTask.NAME) + project.tasks.register(BunInstallTask.NAME) project.tasks.register(NodeSetupTask.NAME) project.tasks.register(NpmSetupTask.NAME) project.tasks.register(PnpmSetupTask.NAME) project.tasks.register(YarnSetupTask.NAME) + project.tasks.register(BunSetupTask.NAME) } private fun addNpmRule(enableTaskRules: Property) { // note this rule also makes it possible to specify e.g. "dependsOn npm_install" @@ -197,5 +205,6 @@ class NodePlugin : Plugin { const val NPM_GROUP = "npm" const val PNPM_GROUP = "pnpm" const val YARN_GROUP = "Yarn" + const val BUN_GROUP = "Bun" } } diff --git a/src/main/kotlin/com/github/gradle/node/bun/exec/BunExecRunner.kt b/src/main/kotlin/com/github/gradle/node/bun/exec/BunExecRunner.kt new file mode 100644 index 00000000..c4c9bf59 --- /dev/null +++ b/src/main/kotlin/com/github/gradle/node/bun/exec/BunExecRunner.kt @@ -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 { + 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 { + 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> { + return nodeExtension.download.flatMap { download -> + if (!download) { + providers.provider { listOf() } + } + val bunDirProvider = variantComputer.computeBunDir(nodeExtension) + val bunBinDirProvider = variantComputer.computeBunBinDir(bunDirProvider, nodeExtension.resolvedPlatform) + bunBinDirProvider.map { file -> listOf(file.asFile.absolutePath) } + } + } +} diff --git a/src/main/kotlin/com/github/gradle/node/bun/task/BunAbstractTask.kt b/src/main/kotlin/com/github/gradle/node/bun/task/BunAbstractTask.kt new file mode 100644 index 00000000..6ad3d8ef --- /dev/null +++ b/src/main/kotlin/com/github/gradle/node/bun/task/BunAbstractTask.kt @@ -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() + + @get:Input + val ignoreExitValue = objects.property().convention(false) + + @get:Input + val environment = objects.mapProperty() + + @get:Internal + val workingDir = objects.directoryProperty() + + @get:Internal + val execOverrides = objects.property>() + + @get:Internal + val projectHelper = project.objects.newInstance() + + @get:Internal + val nodeExtension = NodeExtension[project] + + init { + group = NodePlugin.BUN_GROUP + dependsOn(BunSetupTask.NAME) + } + + // For DSL + @Suppress("unused") + fun execOverrides(execOverrides: Action) { + this.execOverrides.set(execOverrides) + } +} diff --git a/src/main/kotlin/com/github/gradle/node/bun/task/BunInstallTask.kt b/src/main/kotlin/com/github/gradle/node/bun/task/BunInstallTask.kt new file mode 100644 index 00000000..ad8b1ce7 --- /dev/null +++ b/src/main/kotlin/com/github/gradle/node/bun/task/BunInstallTask.kt @@ -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>() + + + 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 { + 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 { + 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 { + 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) { + this.nodeModulesOutputFilter.set(nodeModulesOutputFilter) + } + + companion object { + const val NAME = "bunInstall" + } + +} diff --git a/src/main/kotlin/com/github/gradle/node/bun/task/BunSetupTask.kt b/src/main/kotlin/com/github/gradle/node/bun/task/BunSetupTask.kt new file mode 100644 index 00000000..50565928 --- /dev/null +++ b/src/main/kotlin/com/github/gradle/node/bun/task/BunSetupTask.kt @@ -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 { + return nodeExtension.bunVersion + } + + @get:OutputDirectory + val bunDir by lazy { + val variantComputer = VariantComputer() + variantComputer.computeBunDir(nodeExtension) + } + + override fun computeCommand(): List { + 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" + } + +} diff --git a/src/main/kotlin/com/github/gradle/node/bun/task/BunTask.kt b/src/main/kotlin/com/github/gradle/node/bun/task/BunTask.kt new file mode 100644 index 00000000..693937b2 --- /dev/null +++ b/src/main/kotlin/com/github/gradle/node/bun/task/BunTask.kt @@ -0,0 +1,26 @@ +package com.github.gradle.node.bun.task + +import com.github.gradle.node.bun.exec.BunExecRunner +import com.github.gradle.node.exec.NodeExecConfiguration +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.TaskAction +import org.gradle.kotlin.dsl.listProperty + +abstract class BunTask : BunAbstractTask() { + @get:Optional + @get:Input + val bunCommand = objects.listProperty() + + @TaskAction + fun exec() { + val command = bunCommand.get().plus(args.get()) + val nodeExecConfiguration = + NodeExecConfiguration( + command, environment.get(), workingDir.asFile.orNull, + ignoreExitValue.get(), execOverrides.orNull + ) + val bunExecRunner = objects.newInstance(BunExecRunner::class.java) + result = bunExecRunner.executeBunCommand(projectHelper, nodeExtension, nodeExecConfiguration, variantComputer) + } +} diff --git a/src/main/kotlin/com/github/gradle/node/bun/task/BunxTask.kt b/src/main/kotlin/com/github/gradle/node/bun/task/BunxTask.kt new file mode 100644 index 00000000..68380b7a --- /dev/null +++ b/src/main/kotlin/com/github/gradle/node/bun/task/BunxTask.kt @@ -0,0 +1,24 @@ +package com.github.gradle.node.bun.task + +import com.github.gradle.node.bun.exec.BunExecRunner +import com.github.gradle.node.exec.NodeExecConfiguration +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction +import org.gradle.kotlin.dsl.property + +abstract class BunxTask : BunAbstractTask() { + @get:Input + val command = objects.property() + + @TaskAction + fun exec() { + val command = command.map { listOf(it) }.get().plus(args.get()) + val nodeExecConfiguration = + NodeExecConfiguration( + command, environment.get(), workingDir.asFile.orNull, + ignoreExitValue.get(), execOverrides.orNull + ) + val bunExecRunner = objects.newInstance(BunExecRunner::class.java) + result = bunExecRunner.executeBunxCommand(projectHelper, nodeExtension, nodeExecConfiguration, variantComputer) + } +} diff --git a/src/main/kotlin/com/github/gradle/node/variant/VariantComputer.kt b/src/main/kotlin/com/github/gradle/node/variant/VariantComputer.kt index f713ef62..1ea383b2 100644 --- a/src/main/kotlin/com/github/gradle/node/variant/VariantComputer.kt +++ b/src/main/kotlin/com/github/gradle/node/variant/VariantComputer.kt @@ -116,6 +116,21 @@ open class VariantComputer { } } + /** + * Get the expected bunx binary name, bunx.cmd on Windows and bunx everywhere else. + * + * Can be overridden by setting bunxCommand. + */ + fun computeBunxExec(nodeExtension: NodeExtension, bunBinDirProvider: Provider): Provider { + return zip(nodeExtension.download, nodeExtension.npxCommand, bunBinDirProvider).map { + val (download, bunxCommand, bunBinDir) = it + val command = if (nodeExtension.resolvedPlatform.get().isWindows()) { + bunxCommand.mapIf({ it == "bunx" }) { "bunx.cmd" } + } else bunxCommand + if (download) bunBinDir.dir(command).asFile.absolutePath else command + } + } + fun computePnpmDir(nodeExtension: NodeExtension): Provider { return zip(nodeExtension.pnpmVersion, nodeExtension.pnpmWorkDir).map { val (pnpmVersion, pnpmWorkDir) = it @@ -163,6 +178,29 @@ open class VariantComputer { } } + fun computeBunDir(nodeExtension: NodeExtension): Provider { + return zip(nodeExtension.bunVersion, nodeExtension.bunWorkDir).map { + val (bunVersion, bunWorkDir) = it + val dirnameSuffix = if (bunVersion.isNotBlank()) { + "-v${bunVersion}" + } else "-latest" + val dirname = "bun$dirnameSuffix" + bunWorkDir.dir(dirname) + } + } + + fun computeBunBinDir(bunDirProvider: Provider, platform: Property) = computeProductBinDir(bunDirProvider, platform) + + fun computeBunExec(nodeExtension: NodeExtension, bunBinDirProvider: Provider): Provider { + return zip(nodeExtension.bunCommand, nodeExtension.download, bunBinDirProvider).map { + val (bunCommand, download, bunBinDir) = it + val command = if (nodeExtension.resolvedPlatform.get().isWindows()) { + bunCommand.mapIf({ it == "bun" }) { "bun.cmd" } + } else bunCommand + if (download) bunBinDir.dir(command).asFile.absolutePath else command + } + } + private fun computeProductBinDir(productDirProvider: Provider, platform: Property) = if (platform.get().isWindows()) productDirProvider else productDirProvider.map { it.dir("bin") } diff --git a/src/test/groovy/com/github/gradle/node/bun/task/BunInstall_integTest.groovy b/src/test/groovy/com/github/gradle/node/bun/task/BunInstall_integTest.groovy new file mode 100644 index 00000000..dfd8aac2 --- /dev/null +++ b/src/test/groovy/com/github/gradle/node/bun/task/BunInstall_integTest.groovy @@ -0,0 +1,251 @@ +package com.github.gradle.node.bun.task + +import com.github.gradle.AbstractIntegTest +import org.gradle.testkit.runner.TaskOutcome +import spock.lang.IgnoreIf + +import static com.github.gradle.node.NodeExtension.DEFAULT_NODE_VERSION + +@IgnoreIf({ os.windows }) +class BunInstall_integTest extends AbstractIntegTest { + def 'install packages with bun (#gv.version)'() { + given: + gradleVersion = gv + + writeBuild(''' + plugins { + id 'com.github.node-gradle.node' + } + ''') + writeEmptyPackageJson() + + when: + def result = build('bunInstall') + + then: + result.task(":nodeSetup").outcome == TaskOutcome.SKIPPED + result.task(":bunSetup").outcome == TaskOutcome.SUCCESS + result.task(":bunInstall").outcome == TaskOutcome.SUCCESS + + when: + result = build('bunInstall') + + then: + result.task(":nodeSetup").outcome == TaskOutcome.SKIPPED + result.task(":bunSetup").outcome == TaskOutcome.UP_TO_DATE + // because bun.lockb is generated only when needed + result.task(":bunInstall").outcome == TaskOutcome.UP_TO_DATE + + where: + gv << GRADLE_VERSIONS_UNDER_TEST + } + + def 'install packages with bun in different directory (#gv.version)'() { + given: + gradleVersion = gv + writeBuild( ''' + plugins { + id 'com.github.node-gradle.node' + } + + node { + download = true + workDir = file('build/node') + bunWorkDir = file('build/bundir') + nodeProjectDir = file('subdirectory') + } + ''' ) + writeFile( 'subdirectory/package.json', """{ + "name": "example", + "dependencies": {} + }""" ) + + when: + def result = build( 'bunInstall' ) + + then: + result.task( ':bunInstall' ).outcome == TaskOutcome.SUCCESS + + where: + gv << GRADLE_VERSIONS_UNDER_TEST + } + + def 'verify bun install inputs/outputs (#gv.version)'() { + given: + gradleVersion = gv + writeBuild( ''' + plugins { + id 'com.github.node-gradle.node' + } + + node { + download = true + workDir = file('build/node') + npmInstallCommand = 'install' + } + + def lock = file('bun.lockb') + def installTask = tasks.named("bunInstall").get() + def outputs = installTask.outputs.files + def inputs = installTask.inputs.files + task verifyIO { + doLast { + if (!outputs.contains(lock)) { + throw new RuntimeException("bun.lockb is not in INSTALL'S outputs!") + } + if (inputs.contains(lock)) { + throw new RuntimeException("bun.lockb is in INSTALL'S inputs!") + } + } + } + ''' ) + writeEmptyPackageJson() + writeFile('bun.lockb', '') + + when: + def result = buildTask( 'verifyIO' ) + + then: + result.outcome == TaskOutcome.SUCCESS + + where: + gv << GRADLE_VERSIONS_UNDER_TEST + } + + def 'verify output configuration (#gv.version)'() { + given: + gradleVersion = gv + writeBuild(''' + plugins { + id 'com.github.node-gradle.node' + } + + node { + download = true + workDir = file('build/node') + bunWorkDir = file('build/bundir') + } + ''') + writePackageJson(""" + { + "name": "hello", + "dependencies": { + "is-number": "7.0.0" + } + } + """) + + when: + def result1 = build("bunInstall") + + then: + result1.task(":bunInstall").outcome == TaskOutcome.SUCCESS + + when: + def result2 = build("bunInstall") + + then: + // Because bun.lockb was created + result2.task(":bunInstall").outcome == TaskOutcome.SUCCESS + + when: + // Let's add a file in the node_modules directory + writeFile("node_modules/is-number/newFile.txt", "hello") + def result3 = build("bunInstall") + + then: + // It should not make the build out-of-date + result3.task(":bunInstall").outcome == TaskOutcome.UP_TO_DATE + + when: + // Let's update a file in the node_modules directory + createFile("node_modules/is-number/README.md").delete() + writeFile("node_modules/is-number/README.md", "modified README") + def result4 = build("bunInstall") + + then: + // This time the build should not be up-to-date and the file could (but it's not) reset + result4.task(":bunInstall").outcome == TaskOutcome.SUCCESS + createFile("node_modules/is-number/README.md").text == "modified README" + + when: + // Let's delete a file in the node_modules directory + createFile("node_modules/is-number/README.md").delete() + def result5 = build("bunInstall") + + then: + // This time the build should not be up-to-date and the file should be reset + result5.task(":bunInstall").outcome == TaskOutcome.SUCCESS + createFile("node_modules/is-number/package.json").exists() + + where: + gv << GRADLE_VERSIONS_UNDER_TEST + } + + def 'verify output configuration when filtering node_modules output (#gv.version)'() { + given: + gradleVersion = gv + writeBuild( ''' + plugins { + id 'com.github.node-gradle.node' + } + + node { + download = true + workDir = file('build/node') + } + + bunInstall { + nodeModulesOutputFilter { + exclude("is-number/package.json") + } + } + ''' ) + writePackageJson(""" + { + "name": "hello", + "dependencies": { + "is-number": "7.0.0" + } + } + """) + + when: + createFile("node_modules").deleteDir() + def result1 = build("bunInstall") + + then: + result1.task(":bunInstall").outcome == TaskOutcome.SUCCESS + + when: + // Let's add a file in the node_modules directory + writeFile("node_modules/is-number/newFile.txt", "hello") + def result2 = build("bunInstall") + + then: + // It should make the build out-of-date + result2.task(":bunInstall").outcome == TaskOutcome.SUCCESS + + when: + // Let's update a file in the node_modules directory + createFile("node_modules/is-number/README.md").delete() + writeFile("node_modules/is-number/README.md", "modified README") + def result3 = build("bunInstall") + + then: + // It should make the build out-of-date + result3.task(":bunInstall").outcome == TaskOutcome.SUCCESS + + when: + // Let's delete an excluded file in the node_modules directory + createFile("node_modules/is-number/package.json").delete() + def result4 = build("bunInstall") + + then: + // The build should still be up-to-date + result4.task(":bunInstall").outcome == TaskOutcome.UP_TO_DATE + + where: + gv << GRADLE_VERSIONS_UNDER_TEST + } +} diff --git a/src/test/groovy/com/github/gradle/node/bun/task/BunSetupTaskTest.groovy b/src/test/groovy/com/github/gradle/node/bun/task/BunSetupTaskTest.groovy new file mode 100644 index 00000000..83770a40 --- /dev/null +++ b/src/test/groovy/com/github/gradle/node/bun/task/BunSetupTaskTest.groovy @@ -0,0 +1,47 @@ +package com.github.gradle.node.bun.task + +import com.github.gradle.node.task.AbstractTaskTest + +class BunSetupTaskTest + extends AbstractTaskTest +{ + def "exec bunSetup task without any bun version specified"() { + given: + def task = project.tasks.create('simple', BunSetupTask) + mockProjectApiHelperExec(task) + + when: + project.evaluate() + task.exec() + + then: + 1 * execSpec.setArgs({ args -> + def expectedBunInstallPath = projectDir.toPath().resolve('.gradle').resolve('bun') + .resolve('bun-latest').toAbsolutePath().toString() + def expectedArgs = ['install', '--global', '--no-save', '--prefix', expectedBunInstallPath, 'bun'] + // Workaround a strange issue on Github actions macOS hosts + return args.collect { it.replace("^/private/", "/") } == expectedArgs + }) + } + + def "exec bunSetup task with bun version specified"() { + given: + def bunVersion = '1.0.0' + nodeExtension.bunVersion.set(bunVersion) + def task = project.tasks.create('simple', BunSetupTask) + mockProjectApiHelperExec(task) + + when: + project.evaluate() + task.exec() + + then: + 1 * execSpec.setArgs({ args -> + def expectedBunInstallPath = projectDir.toPath().resolve('.gradle').resolve('bun') + .resolve("pnpm-v${bunVersion}").toAbsolutePath().toString() + def expectedArgs = ['install', '--global', '--no-save', '--prefix', expectedBunInstallPath, "bun@${bunVersion}"] + // Workaround a strange issue on Github actions macOS hosts + return args.collect { it.replace("^/private/", "/") } == expectedArgs + }) + } +} diff --git a/src/test/groovy/com/github/gradle/node/bun/task/BunTaskTest.groovy b/src/test/groovy/com/github/gradle/node/bun/task/BunTaskTest.groovy new file mode 100644 index 00000000..252a727e --- /dev/null +++ b/src/test/groovy/com/github/gradle/node/bun/task/BunTaskTest.groovy @@ -0,0 +1,69 @@ +package com.github.gradle.node.bun.task + +import com.github.gradle.node.pnpm.task.PnpmTask +import com.github.gradle.node.task.AbstractTaskTest +import com.github.gradle.node.util.PlatformHelperKt + +class BunTaskTest extends AbstractTaskTest { + def "exec bun task"() { + given: + nodeExtension.resolvedPlatform.set(PlatformHelperKt.parsePlatform("Linux", "x86_64", {})) + + def task = project.tasks.create('simple', BunTask) + mockProjectApiHelperExec(task) + task.args.set(['a', 'b']) + task.environment.set(['a': '1']) + task.ignoreExitValue.set(true) + task.workingDir.set(projectDir) + + when: + project.evaluate() + task.exec() + + then: + task.args.set(['a', 'b']) + 1 * execSpec.setIgnoreExitValue(true) + 1 * execSpec.setEnvironment({ it['a'] == '1' && containsPath(it) }) + 1 * execSpec.setExecutable('bun') + 1 * execSpec.setArgs(['a', 'b']) + } + + def "exec bun task (windows)"() { + given: + nodeExtension.resolvedPlatform.set(PlatformHelperKt.parsePlatform("Windows", "x86_64", {})) + + def task = project.tasks.create('simple', BunTask) + mockProjectApiHelperExec(task) + task.args.set(['a', 'b']) + task.environment.set(['a': '1']) + task.ignoreExitValue.set(true) + task.workingDir.set(projectDir) + + when: + project.evaluate() + task.exec() + + then: + task.args.get() == ['a', 'b'] + 1 * execSpec.setIgnoreExitValue(true) + 1 * execSpec.setEnvironment({ it['a'] == '1' && containsPath(it) }) + 1 * execSpec.setExecutable('bun.cmd') + 1 * execSpec.setArgs(['a', 'b']) + } + + def "exec bun task (download)"() { + given: + nodeExtension.resolvedPlatform.set(PlatformHelperKt.parsePlatform("Linux", "x86_64", {})) + nodeExtension.download.set(true) + + def task = project.tasks.create('simple', PnpmTask) + mockProjectApiHelperExec(task) + + when: + project.evaluate() + task.exec() + + then: + 1 * execSpec.setIgnoreExitValue(false) + } +} diff --git a/src/test/groovy/com/github/gradle/node/bun/task/BunTask_integTest.groovy b/src/test/groovy/com/github/gradle/node/bun/task/BunTask_integTest.groovy new file mode 100644 index 00000000..64d29f3b --- /dev/null +++ b/src/test/groovy/com/github/gradle/node/bun/task/BunTask_integTest.groovy @@ -0,0 +1,172 @@ +package com.github.gradle.node.bun.task + +import com.github.gradle.AbstractIntegTest +import com.github.gradle.node.Versions +import org.gradle.testkit.runner.TaskOutcome +import org.junit.Rule +import org.junit.contrib.java.lang.system.EnvironmentVariables +import spock.lang.Ignore +import spock.lang.IgnoreIf + +@IgnoreIf({ os.windows }) +class BunTask_integTest extends AbstractIntegTest { + def 'execute bun command with a package.json file and check inputs up-to-date detection (#gv.version)'() { + given: + gradleVersion = gv + copyResources('fixtures/bun/', '') + copyResources('fixtures/javascript-project/', '') + + when: + def result1 = build(":test") + + then: + result1.task(":nodeSetup").outcome == TaskOutcome.SKIPPED + result1.task(":bunSetup").outcome == TaskOutcome.SUCCESS + result1.task(":bunInstall").outcome == TaskOutcome.SUCCESS + result1.task(":test").outcome == TaskOutcome.SUCCESS + result1.output.contains("1 passing") + + when: + def result2 = build(":test") + + then: + result2.task(":nodeSetup").outcome == TaskOutcome.SKIPPED + result2.task(":bunSetup").outcome == TaskOutcome.UP_TO_DATE + result2.task(":bunInstall").outcome == TaskOutcome.SUCCESS + result2.task(":test").outcome == TaskOutcome.UP_TO_DATE + + when: + def result3 = build(":test", "-DchangeInputs=true") + + then: + result3.task(":nodeSetup").outcome == TaskOutcome.SKIPPED + result3.task(":bunSetup").outcome == TaskOutcome.UP_TO_DATE + result3.task(":bunInstall").outcome == TaskOutcome.UP_TO_DATE + result3.task(":test").outcome == TaskOutcome.SUCCESS + + when: + def result4 = build(":version") + + then: + result4.task(":version").outcome == TaskOutcome.SUCCESS + result4.output.contains("> Task :version${System.lineSeparator()}1.0.3") + + where: + gv << GRADLE_VERSIONS_UNDER_TEST + } + + def 'execute bun command with custom execution configuration and check up-to-date-detection (#gv.version)'() { + given: + gradleVersion = gv + copyResources('fixtures/bun-env/', '') + + when: + def result1 = build(":env") + + then: + result1.task(":nodeSetup").outcome == TaskOutcome.SKIPPED + result1.task(":bunSetup").outcome == TaskOutcome.SUCCESS + result1.task(":bunInstall").outcome == TaskOutcome.SUCCESS + result1.task(":env").outcome == TaskOutcome.SUCCESS + environmentDumpContainsPathVariable(result1.output) + + when: + def result2 = build(":env", "-DcustomEnv=true") + + then: + result2.task(":nodeSetup").outcome == TaskOutcome.SKIPPED + result2.task(":bunSetup").outcome == TaskOutcome.UP_TO_DATE + result2.task(":bunInstall").outcome == TaskOutcome.SUCCESS + result2.task(":env").outcome == TaskOutcome.SUCCESS + result2.output.contains("CUSTOM=custom value") + + when: + System.setProperty("NEW_ENV_VARIABLE", "Let's make the whole environment change") + def result3 = build(":env", "-DcustomEnv=true") + + then: + result3.task(":nodeSetup").outcome == TaskOutcome.SKIPPED + result3.task(":bunSetup").outcome == TaskOutcome.UP_TO_DATE + result3.task(":bunInstall").outcome == TaskOutcome.UP_TO_DATE + result3.task(":env").outcome == TaskOutcome.UP_TO_DATE + + when: + def result4 = build(":env", "-DignoreExitValue=true", "-DnotExistingCommand=true") + + then: + result4.task(":nodeSetup").outcome == TaskOutcome.SKIPPED + result4.task(":bunSetup").outcome == TaskOutcome.UP_TO_DATE + result4.task(":bunInstall").outcome == TaskOutcome.UP_TO_DATE + result4.task(":env").outcome == TaskOutcome.SUCCESS + result4.output.contains("script not found \"notExistingCommand\"") + + when: + def result5 = buildAndFail(":env", "-DnotExistingCommand=true") + + then: + result5.task(":nodeSetup").outcome == TaskOutcome.SKIPPED + result5.task(":bunSetup").outcome == TaskOutcome.UP_TO_DATE + result5.task(":bunInstall").outcome == TaskOutcome.UP_TO_DATE + result5.task(":env").outcome == TaskOutcome.FAILED + result5.output.contains("script not found \"notExistingCommand\"") + + when: + def result6 = build(":pwd") + + then: + result6.task(":nodeSetup").outcome == TaskOutcome.SKIPPED + result6.task(":bunSetup").outcome == TaskOutcome.UP_TO_DATE + result6.task(":bunInstall").outcome == TaskOutcome.UP_TO_DATE + result6.task(":pwd").outcome == TaskOutcome.SUCCESS + result6.output.contains("Working directory is '${projectDir}'") + + when: + def result7 = build(":pwd", "-DcustomWorkingDir=true") + + then: + result7.task(":nodeSetup").outcome == TaskOutcome.SKIPPED + result7.task(":bunSetup").outcome == TaskOutcome.UP_TO_DATE + result7.task(":bunInstall").outcome == TaskOutcome.UP_TO_DATE + result7.task(":pwd").outcome == TaskOutcome.UP_TO_DATE + + when: + def result8 = build(":pwd", "-DcustomWorkingDir=true", "--rerun-tasks") + + then: + result8.task(":nodeSetup").outcome == TaskOutcome.SKIPPED + result8.task(":bunSetup").outcome == TaskOutcome.SUCCESS + result8.task(":bunInstall").outcome == TaskOutcome.SUCCESS + result8.task(":pwd").outcome == TaskOutcome.SUCCESS + def expectedWorkingDirectory = "${projectDir}${File.separator}build${File.separator}customWorkingDirectory" + result8.output.contains("Working directory is '${expectedWorkingDirectory}'") + new File(expectedWorkingDirectory).isDirectory() + + when: + def result9 = build(":version") + + then: + result9.task(":version").outcome == TaskOutcome.SUCCESS + result9.output.contains("> Task :version${System.lineSeparator()}1.0.3") + + where: + gv << GRADLE_VERSIONS_UNDER_TEST + } + + @Ignore("Should it even work that way?") + def 'execute bun command using the bun version specified in the package.json file (#gv.version)'() { + given: + gradleVersion = gv + copyResources('fixtures/bun/', '') + copyResources('fixtures/bun-present/', '') + + when: + def result = build(":version") + + then: + result.task(":version").outcome == TaskOutcome.SUCCESS + result.output.contains("> Task :version${System.lineSeparator()}1.0.0") + + where: + gv << GRADLE_VERSIONS_UNDER_TEST + } +} diff --git a/src/test/groovy/com/github/gradle/node/bun/task/Bun_integTest.groovy b/src/test/groovy/com/github/gradle/node/bun/task/Bun_integTest.groovy new file mode 100644 index 00000000..f0e7b25e --- /dev/null +++ b/src/test/groovy/com/github/gradle/node/bun/task/Bun_integTest.groovy @@ -0,0 +1,50 @@ +package com.github.gradle.node.bun.task + +import com.github.gradle.AbstractIntegTest +import org.gradle.testkit.runner.TaskOutcome +import spock.lang.IgnoreIf + +@IgnoreIf({ os.windows }) +class Bun_integTest extends AbstractIntegTest { + def 'install packages with Bun and project in sub directory (#gv.version)'() { + given: + gradleVersion = gv + + copyResources("fixtures/bun-in-subdirectory/") + copyResources("fixtures/javascript-project/", "javascript-project") + + when: + def result1 = build("build") + + then: + result1.task(":bunInstall").outcome == TaskOutcome.SUCCESS + result1.task(":buildBunx").outcome == TaskOutcome.SUCCESS + result1.task(":buildBun").outcome == TaskOutcome.SUCCESS + createFile("javascript-project/bun.lockb").isFile() + createFile("javascript-project/node_modules").isDirectory() + !createFile("bun.lockb").exists() + !createFile("node_modules").exists() + createFile("javascript-project/output-bunx/index.js").isFile() + createFile("javascript-project/output-bun/index.js").isFile() + + when: + def result2 = build("build") + + then: + // Not up-to-date because the bun.lockb now exists + result2.task(":bunInstall").outcome == TaskOutcome.SUCCESS + result2.task(":buildBunx").outcome == TaskOutcome.UP_TO_DATE + result2.task(":buildBun").outcome == TaskOutcome.UP_TO_DATE + + when: + def result3 = build("build") + + then: + result3.task(":bunInstall").outcome == TaskOutcome.UP_TO_DATE + result3.task(":buildBunx").outcome == TaskOutcome.UP_TO_DATE + result3.task(":buildBun").outcome == TaskOutcome.UP_TO_DATE + + where: + gv << GRADLE_VERSIONS_UNDER_TEST + } +} diff --git a/src/test/groovy/com/github/gradle/node/bun/task/BunxTask_integTest.groovy b/src/test/groovy/com/github/gradle/node/bun/task/BunxTask_integTest.groovy new file mode 100644 index 00000000..558d4bdf --- /dev/null +++ b/src/test/groovy/com/github/gradle/node/bun/task/BunxTask_integTest.groovy @@ -0,0 +1,216 @@ +package com.github.gradle.node.bun.task + +import com.github.gradle.AbstractIntegTest +import com.github.gradle.node.NodeExtension +import org.gradle.testkit.runner.TaskOutcome +import org.junit.Rule +import org.junit.contrib.java.lang.system.EnvironmentVariables +import spock.lang.Ignore +import spock.lang.IgnoreIf + +import java.util.regex.Pattern + +import static com.github.gradle.node.NodeExtension.DEFAULT_NPM_VERSION + +class BunxTask_integTest extends AbstractIntegTest { + def 'execute bunx command with no package.json file (#gv.version)'() { + given: + gradleVersion = gv + + writeBuild(''' + plugins { + id 'com.github.node-gradle.node' + } + + task camelCase(type: BunxTask) { + command = 'chcase-cli' + args = ['--help'] + } + ''') + + when: + def result = build(":camelCase") + + then: + result.task(":nodeSetup").outcome == TaskOutcome.SKIPPED + result.task(":bunSetup").outcome == TaskOutcome.SUCCESS + result.task(":camelCase").outcome == TaskOutcome.SUCCESS + result.output.contains("--case, -C Which case to convert to") + + where: + gv << GRADLE_VERSIONS_UNDER_TEST + } + + def 'execute bunx command with a package.json file and check inputs up-to-date detection (#gv.version)'() { + given: + gradleVersion = gv + + copyResources("fixtures/bunx/") + copyResources("fixtures/javascript-project/") + + when: + def result1 = build(":test") + + then: + result1.task(":nodeSetup").outcome == TaskOutcome.SKIPPED + result1.task(":bunSetup").outcome == TaskOutcome.SUCCESS + result1.task(":bunInstall").outcome == TaskOutcome.SUCCESS + result1.task(":lint").outcome == TaskOutcome.SUCCESS + result1.task(":test").outcome == TaskOutcome.SUCCESS + result1.output.contains("5 problems (0 errors, 5 warnings)") + result1.output.contains("1 passing") + + when: + def result2 = build(":test") + + then: + result2.task(":nodeSetup").outcome == TaskOutcome.SKIPPED + result2.task(":bunSetup").outcome == TaskOutcome.UP_TO_DATE + result2.task(":bunInstall").outcome == TaskOutcome.SUCCESS + result2.task(":lint").outcome == TaskOutcome.UP_TO_DATE + result2.task(":test").outcome == TaskOutcome.UP_TO_DATE + + when: + def result3 = build(":test", "-DchangeInputs=true") + + then: + result3.task(":nodeSetup").outcome == TaskOutcome.SKIPPED + result3.task(":bunSetup").outcome == TaskOutcome.UP_TO_DATE + result3.task(":bunInstall").outcome == TaskOutcome.UP_TO_DATE + result3.task(":lint").outcome == TaskOutcome.SUCCESS + result3.task(":test").outcome == TaskOutcome.SUCCESS + + where: + gv << GRADLE_VERSIONS_UNDER_TEST + } + + def 'execute bun pwd command with custom execution configuration and check up-to-date-detection'() { + given: + gradleVersion = gv + + copyResources("fixtures/bunx-env/") + copyResources("fixtures/env/") + + when: + def result7 = build(":pwd") + + then: + result7.task(":pwd").outcome == TaskOutcome.SUCCESS + result7.output.contains("workingDirectory='${projectDir}'") + + when: + def result8 = build(":pwd", "-DcustomWorkingDir=true") + + then: + result8.task(":pwd").outcome == TaskOutcome.UP_TO_DATE + + when: + def result9 = build(":pwd", "-DcustomWorkingDir=true", "--rerun-tasks") + + then: + result9.task(":nodeSetup").outcome == TaskOutcome.SKIPPED + result9.task(":bunSetup").outcome == TaskOutcome.SUCCESS + result9.task(":bunInstall").outcome == TaskOutcome.SUCCESS + result9.task(":pwd").outcome == TaskOutcome.SUCCESS + def expectedWorkingDirectory = "${projectDir}${File.separator}build${File.separator}customWorkingDirectory" + result9.output.contains("workingDirectory='${expectedWorkingDirectory}'") + new File(expectedWorkingDirectory).isDirectory() + + where: + gv << GRADLE_VERSIONS_UNDER_TEST + } + + def 'execute bunx env command with custom execution configuration and check up-to-date-detection'() { + given: + gradleVersion = gv + + copyResources("fixtures/bunx-env/") + copyResources("fixtures/bun-env/package.json", "package.json") + + when: + def result1 = build(":env") + + then: + result1.task(":nodeSetup").outcome == TaskOutcome.SKIPPED + result1.task(":bunSetup").outcome == TaskOutcome.SUCCESS + result1.task(":bunInstall").outcome == TaskOutcome.SUCCESS + result1.task(":env").outcome == TaskOutcome.SUCCESS + // Sometimes the PATH variable is not defined in Windows Powershell, but the PATHEXT is + Pattern.compile("^PATH(?:EXT)?=.+\$", Pattern.MULTILINE).matcher(result1.output).find() + + when: + def result2 = build(":env", "-DcustomEnv=true") + + then: + result2.task(":nodeSetup").outcome == TaskOutcome.SKIPPED + result2.task(":bunSetup").outcome == TaskOutcome.UP_TO_DATE + result2.task(":bunInstall").outcome == TaskOutcome.SUCCESS + result2.task(":env").outcome == TaskOutcome.SUCCESS + result2.output.contains("CUSTOM=custom value") + + when: + System.setProperty("NEW_ENV_VARIABLE", "Let's make the whole environment change") + def result3 = build(":env", "-DcustomEnv=true") + + then: + result3.task(":nodeSetup").outcome == TaskOutcome.SKIPPED + result3.task(":bunSetup").outcome == TaskOutcome.UP_TO_DATE + result3.task(":bunInstall").outcome == TaskOutcome.UP_TO_DATE + result3.task(":env").outcome == TaskOutcome.UP_TO_DATE + + when: + def result4 = build(":env", "-DignoreExitValue=true", "-DnotExistingCommand=true") + + then: + result4.task(":nodeSetup").outcome == TaskOutcome.SKIPPED + result4.task(":bunSetup").outcome == TaskOutcome.UP_TO_DATE + result4.task(":bunInstall").outcome == TaskOutcome.UP_TO_DATE + result4.task(":env").outcome == TaskOutcome.SUCCESS + result4.output.contains("E404") + + when: + def result5 = buildAndFail(":env", "-DnotExistingCommand=true") + + then: + result5.task(":nodeSetup").outcome == TaskOutcome.SKIPPED + result5.task(":bunSetup").outcome == TaskOutcome.UP_TO_DATE + result5.task(":bunInstall").outcome == TaskOutcome.UP_TO_DATE + result5.task(":env").outcome == TaskOutcome.FAILED + result5.output.contains("E404") + + when: + def result6 = build(":env", "-DoutputFile=true", "--stacktrace") + + then: + result6.task(":nodeSetup").outcome == TaskOutcome.SKIPPED + result6.task(":bunSetup").outcome == TaskOutcome.UP_TO_DATE + result6.task(":bunInstall").outcome == TaskOutcome.UP_TO_DATE + result6.task(":env").outcome == TaskOutcome.SUCCESS + !environmentDumpContainsPathVariable(result6.output) + def outputFile = file("build/standard-output.txt") + outputFile.exists() + environmentDumpContainsPathVariable(outputFile.text) + + where: + gv << GRADLE_VERSIONS_UNDER_TEST + } + + @Ignore("Should it even work that way?") + def 'execute bunx command using the npm version specified in the package.json file (#gv.version)'() { + given: + gradleVersion = gv + + copyResources("fixtures/bunx/") + copyResources("fixtures/bun-present/") + + when: + def result = build(":version") + + then: + result.task(":version").outcome == TaskOutcome.SUCCESS + result.output.contains("> Task :version${System.lineSeparator()}1.0.0") + + where: + gv << GRADLE_VERSIONS_UNDER_TEST + } +} diff --git a/src/test/resources/fixtures/bun-env/build.gradle b/src/test/resources/fixtures/bun-env/build.gradle new file mode 100644 index 00000000..507a14a0 --- /dev/null +++ b/src/test/resources/fixtures/bun-env/build.gradle @@ -0,0 +1,65 @@ +plugins { + id "com.github.node-gradle.node" +} + +node { + workDir = file("build/node") + bunVersion = '1.0.3' +} + +task env(type: BunTask) { + dependsOn bunInstall + bunCommand = ["run", "print-env"] + outputs.upToDateWhen { + true + } +} + +// This should be documented in the package.json file but it cannot be done because JSON does not accept comments. +// bun run forces the current directory but we can read the INIT_CWD environment variable to get the original working +// directory. This is the reason why we read the INIT_CWD variable and not the process.cwd() one in the script +// definition. +task pwd(type: BunTask) { + dependsOn bunInstall + bunCommand = ["run", "printcwd"] + outputs.upToDateWhen { + true + } +} + +task version(type: BunTask) { + dependsOn bunInstall + bunCommand = ["--version"] +} + +if (isPropertyEnabled("customEnv")) { + def qualifier = "custom" + env.environment = [CUSTOM: "${qualifier} value"] +} + +if (isPropertyEnabled("ignoreExitValue")) { + env.ignoreExitValue = true +} + +if (isPropertyEnabled("notExistingCommand")) { + env.bunCommand = ["notExistingCommand"] +} + +if (isPropertyEnabled("customWorkingDir")) { + pwd.workingDir = file("${project.buildDir}/customWorkingDirectory/") +} + +if (isPropertyEnabled("outputFile")) { + env.execOverrides { + standardOutput = new FileOutputStream("${buildDir}/standard-output.txt") + } +} + +def isPropertyEnabled(String name) { + def provider = providers.systemProperty(name) + if (org.gradle.util.GradleVersion.current() >= org.gradle.util.GradleVersion.version("7.4")) { + return provider.isPresent() + } else { + return provider.forUseAtConfigurationTime().isPresent() + } +} diff --git a/src/test/resources/fixtures/bun-env/package.json b/src/test/resources/fixtures/bun-env/package.json new file mode 100644 index 00000000..a57a2ac6 --- /dev/null +++ b/src/test/resources/fixtures/bun-env/package.json @@ -0,0 +1,11 @@ +{ + "name": "env", + "dependencies": { + "@bahmutov/print-env": "2.0.2", + "utils-eval": "1.0.1" + }, + "scripts": { + "print-env": "print-env PATH CUSTOM", + "printcwd": "jseval \"'Working directory is \\'' + process.env.PWD + '\\''\"" + } +} diff --git a/src/test/resources/fixtures/bun-in-subdirectory/build.gradle b/src/test/resources/fixtures/bun-in-subdirectory/build.gradle new file mode 100644 index 00000000..613d4e24 --- /dev/null +++ b/src/test/resources/fixtures/bun-in-subdirectory/build.gradle @@ -0,0 +1,29 @@ +plugins { + id 'com.github.node-gradle.node' +} + +node { + nodeProjectDir = file("${projectDir}/javascript-project") +} + +task buildBunx(type: BunxTask) { + dependsOn bunInstall + command = "babel" + args = ["src", "--out-dir", "output-bunx"] + inputs.dir("javascript-project/src") + inputs.dir("javascript-project/node_modules") + outputs.dir("javascript-project/output-bunx") +} + +task buildBun(type: BunTask) { + dependsOn bunInstall + bunCommand = ["run", "build"] + args = ["--", "--out-dir", "output-bun"] + inputs.dir("javascript-project/src") + inputs.dir("javascript-project/node_modules") + outputs.dir("javascript-project/output-bun") +} + +task build { + dependsOn buildBunx, buildBun +} diff --git a/src/test/resources/fixtures/bun-present/package.json b/src/test/resources/fixtures/bun-present/package.json new file mode 100644 index 00000000..efbb7a1a --- /dev/null +++ b/src/test/resources/fixtures/bun-present/package.json @@ -0,0 +1,9 @@ +{ + "name": "example", + "devDependencies": { + "bun": "1.0.0" + }, + "scripts": { + "bunVersion": "echo Version && bun --version" + } +} diff --git a/src/test/resources/fixtures/bun/build.gradle b/src/test/resources/fixtures/bun/build.gradle new file mode 100644 index 00000000..455d7d77 --- /dev/null +++ b/src/test/resources/fixtures/bun/build.gradle @@ -0,0 +1,36 @@ +plugins { + id 'com.github.node-gradle.node' +} + +def changeInputs = isPropertyEnabled("changeInputs") + +node { + bunVersion = "1.0.3" + workDir = file('build/node') +} + +task test(type: BunTask) { + dependsOn bunInstall + bunCommand = changeInputs ? ['run', 'test'] : ['run'] + args = changeInputs ? [] : ['test'] + inputs.dir('node_modules') + inputs.file('package.json') + inputs.files('index.js', 'test.js') + outputs.upToDateWhen { + true + } +} + +task version(type: BunTask) { + dependsOn bunInstall + bunCommand = ["--version"] +} + +def isPropertyEnabled(String name) { + def provider = providers.systemProperty(name) + if (org.gradle.util.GradleVersion.current() >= org.gradle.util.GradleVersion.version("7.4")) { + return provider.isPresent() + } else { + return provider.forUseAtConfigurationTime().isPresent() + } +} diff --git a/src/test/resources/fixtures/bunx-env/build.gradle b/src/test/resources/fixtures/bunx-env/build.gradle new file mode 100644 index 00000000..7734ab14 --- /dev/null +++ b/src/test/resources/fixtures/bunx-env/build.gradle @@ -0,0 +1,76 @@ +plugins { + id "com.github.node-gradle.node" +} + +node { + workDir = file("build/node") +} + +task env(type: BunxTask) { + dependsOn bunInstall + command = "print-env" + args = ["PATH", "CUSTOM"] + outputs.upToDateWhen { + true + } +} + +task pwd(type: BunxTask) { + dependsOn bunInstall + command = "jseval" + /* This is cursed. + This used to work before we upgraded node+npm but now the backticks break, I tried to work around it and... + ["work..='", process.cwd(), "'"].join('') works, except on windows where it prints: + , process.cwd, + "work..='" + process.cwd() + "'" works, except on windows where it prints: + + process.cwd + + And surprising nobody at this point: + process.stdout.write("workingDirectory='"); process.stdout.write(process.cwd()); process.stdout.write("'\\n"); + Does work, except on windows, where it's parsed as: + process.stdout.write(workingDirectory='); process.stdout.write(process.cwd()); process.stdout.write('\\n); + + If someone manages to fix this, remove the @IgnoreIf from the NpxTask_integTest + */ + args = ["""console.log(["workingDirectory='", process.cwd(), "'"].join(''));"""] + outputs.upToDateWhen { + true + } +} + +task version(type: BunxTask) { + dependsOn bunInstall + command = "--version" +} + +if (isPropertyEnabled("customEnv")) { + def qualifier = "custom" + env.environment = [CUSTOM: "${qualifier} value"] +} + +if (isPropertyEnabled("ignoreExitValue")) { + env.ignoreExitValue = true +} + +if (isPropertyEnabled("notExistingCommand")) { + env.command = "notExistingCommand" +} + +if (isPropertyEnabled("customWorkingDir")) { + pwd.workingDir = file("${project.buildDir}/customWorkingDirectory/") +} + +if (isPropertyEnabled("outputFile")) { + def standardOutputFile = new File(buildDir, "standard-output.txt") + env.execOverrides { + standardOutput = new FileOutputStream(standardOutputFile) + } +} + +def isPropertyEnabled(String name) { + def provider = providers.systemProperty(name) + if (org.gradle.util.GradleVersion.current() >= org.gradle.util.GradleVersion.version("7.4")) { + return provider.isPresent() + } else { + return provider.forUseAtConfigurationTime().isPresent() + } +} diff --git a/src/test/resources/fixtures/bunx/build.gradle b/src/test/resources/fixtures/bunx/build.gradle new file mode 100644 index 00000000..8c71db03 --- /dev/null +++ b/src/test/resources/fixtures/bunx/build.gradle @@ -0,0 +1,61 @@ +plugins { + id "com.github.node-gradle.node" +} + +node { + bunVersion = "1.0.3" + workDir = file("build/node") +} + +// mocha is installed locally whereas eslint is not + +task lint(type: BunxTask) { + dependsOn bunInstall + command = "eslint@6.3.0" + args = ["src", "test"] + inputs.file(".eslintrc.yml") + inputs.dir("src") + inputs.dir("test") + outputs.upToDateWhen { + true + } +} + +task test(type: BunxTask) { + dependsOn lint + command = "mocha" + inputs.file("package.json") + inputs.dir("src") + inputs.dir("test") + outputs.upToDateWhen { + true + } +} + +task env(type: BunxTask) { + command = "print-env" + outputs.upToDateWhen { + true + } +} + +task cwd(type: BunxTask) { + command = "cwd" + outputs.upToDateWhen { + true + } +} + +if (isPropertyEnabled("changeInputs")) { + lint.args = ["src"] + test.command = "_mocha" +} + +def isPropertyEnabled(String name) { + def provider = providers.systemProperty(name) + if (org.gradle.util.GradleVersion.current() >= org.gradle.util.GradleVersion.version("7.4")) { + return provider.isPresent() + } else { + return provider.forUseAtConfigurationTime().isPresent() + } +}