Compare commits

..

7 Commits

Author SHA1 Message Date
hubert a4add8adbc Update dependencies 2023-05-07 18:00:44 +02:00
hubert 8ad1e8d884 Auto discover GRAALVM_HOME 2023-05-07 17:36:39 +02:00
hubert 37dba17074 Upgrade gradle + jdk 2023-05-07 16:56:34 +02:00
hubert e12cb1cac7 Simplify js api 2021-04-03 22:34:16 +02:00
hubert 907fdb4f10 Clean build 2021-04-03 22:24:34 +02:00
hubert f037c6a724 Fix promptBoolean + Update build script 2021-04-02 19:25:01 +02:00
hubert 6ed31db7dc Replace toml config with a javascript file 2021-04-02 18:16:27 +02:00
21 changed files with 283 additions and 141 deletions
+2 -3
View File
@@ -12,9 +12,8 @@ Move the executable somewhere in your $PATH
Or build a native image with GraalVM Or build a native image with GraalVM
```bash ```bash
./gradlew installShadowDist ./gradlew buildNative
cd app/build/install/app-shadow/lib cd app/build/native
native-image --no-fallback -R:MaxNewSize=32 --language:js -jar scaffold.jar
``` ```
## Usage ## Usage
+10 -13
View File
@@ -1,24 +1,21 @@
plugins { plugins {
id("kotlin-application") id("kotlin-application")
id("com.github.johnrengelman.shadow") version "6.1.0" id("shadow")
id("native-image")
id("release")
} }
version = "0.0.1-SNAPSHOT"
dependencies { dependencies {
implementation("org.slf4j:slf4j-api:2.0.0-alpha1") implementation("org.slf4j:slf4j-api:2.0.7")
runtimeOnly("org.slf4j:slf4j-simple:2.0.0-alpha1") runtimeOnly("org.slf4j:slf4j-simple:2.0.7")
implementation("io.pebbletemplates:pebble:3.1.5") implementation("io.pebbletemplates:pebble:3.2.1")
implementation("com.github.ajalt.clikt:clikt:3.1.0") implementation("com.github.ajalt.clikt:clikt:3.5.2")
implementation("org.graalvm.js:js:21.0.0.2") implementation("org.graalvm.js:js:22.3.2")
} }
application { application {
mainClassName = "scaffold.ScaffoldKt"
mainClass.set("scaffold.ScaffoldKt") mainClass.set("scaffold.ScaffoldKt")
applicationName = "scaffold" applicationName = "scaffold"
} }
tasks.withType<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar> {
archiveBaseName.set("scaffold")
archiveClassifier.set("")
archiveVersion.set("")
}
@@ -1,7 +1,4 @@
[ [
{
"name":"com.electronwill.nightconfig.toml.TomlFormat"
},
{ {
"name":"java.util.Map$Entry", "name":"java.util.Map$Entry",
"allPublicMethods":true "allPublicMethods":true
+16 -19
View File
@@ -5,13 +5,7 @@
*/ */
declare function echo(message: string): void declare function echo(message: string): void
/** declare interface prompt {
* Prompt a user for text input
*
* @param text - The text to display for the prompt
* @param def - A default value
*/
declare function prompt(text: string, def?: string): string
/** /**
* Prompt a user for text input * Prompt a user for text input
@@ -19,28 +13,31 @@ declare function prompt(text: string, def?: string): string
* @param text - The text to display for the prompt * @param text - The text to display for the prompt
* @param def - A default value * @param def - A default value
*/ */
declare function promptBoolean(text: string, def?: boolean): boolean string(text: string, def?: string): string
/**
* Prompt a user for text input
*
* @param text - The text to display for the prompt
* @param def - A default value
*/
boolean(text: string, def?: boolean): boolean
}
/** /**
* Renders a template * Renders a template
* *
* @param name - The template name * @param name - The template name
* @param context - The template context * @param context - The template context
* @param output - The output path, if absent, default to the template name
*/ */
declare function render(name: string, context: any): string declare function render(name: string, context: any, output?: string): string
/**
* Write a string to a file
*
* @param input - The content of the file
* @param output - The output path
*/
declare function write(input: string, output: string): void
/** /**
* Copy a file * Copy a file
* *
* @param input - The input path * @param input - The input path
* @param output - The output path * @param output - The output path, if absent, default to the input path
*/ */
declare function copy(input: string, output: string): void declare function copy(input: string, output?: string): void
+2 -2
View File
@@ -1,7 +1,7 @@
echo("Example") echo("Example")
const name = prompt("Project name") const name = prompt.string("Project name")
const ctx = {name: name} const ctx = {name: name}
write(render("README.md", ctx), "README.md") render("README.md", ctx)
+38
View File
@@ -0,0 +1,38 @@
package scaffold
import com.github.ajalt.clikt.core.UsageError
import com.github.ajalt.clikt.output.CliktConsole
import com.github.ajalt.clikt.output.TermUi
import com.github.ajalt.clikt.output.defaultCliktConsole
@Suppress("DEPRECATION") // TODO: later..
class Prompt(private val console: CliktConsole = defaultCliktConsole()) {
fun string(text: String, default: String?): String = TermUi.prompt(
text = text,
default = default,
console = console
) { it }!!
fun boolean(text: String, default: Boolean?): Boolean {
val (defaultString, suffix) = when (default) {
true -> "y" to "[Y/n]"
false -> "n" to "[y/N]"
null -> null to "[y/n]"
}
return TermUi.prompt(
"$text $suffix",
default = defaultString,
showDefault = false,
console = console
) {
when (it.lowercase()) {
"y" -> true
"n" -> false
else -> throw UsageError("Can only be [y/n]")
}
}!!
}
}
+11 -33
View File
@@ -2,17 +2,16 @@ package scaffold.commands
import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.ProgramResult import com.github.ajalt.clikt.core.ProgramResult
import com.github.ajalt.clikt.core.UsageError
import com.github.ajalt.clikt.output.TermUi
import com.github.ajalt.clikt.parameters.arguments.argument import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.options.convert import com.github.ajalt.clikt.parameters.options.convert
import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.required import com.github.ajalt.clikt.parameters.options.required
import com.mitchellbosecke.pebble.PebbleEngine import io.pebbletemplates.pebble.PebbleEngine
import com.mitchellbosecke.pebble.loader.FileLoader import io.pebbletemplates.pebble.loader.FileLoader
import com.mitchellbosecke.pebble.template.PebbleTemplate import io.pebbletemplates.pebble.template.PebbleTemplate
import scaffold.Generator import scaffold.Generator
import scaffold.Generators import scaffold.Generators
import scaffold.Prompt
import scaffold.scripting.ScriptContext import scaffold.scripting.ScriptContext
import scaffold.scripting.ScriptEngine import scaffold.scripting.ScriptEngine
import java.io.StringWriter import java.io.StringWriter
@@ -37,39 +36,18 @@ class GenerateCommand(private val generators: Generators) : CliktCommand("genera
val scriptContext = object : ScriptContext { val scriptContext = object : ScriptContext {
override fun echo(message: String) = this@GenerateCommand.echo(message) override fun echo(message: String) = this@GenerateCommand.echo(message)
override fun prompt(text: String, default: String?) = this@GenerateCommand.prompt(text, default)!! override val prompt = Prompt()
override fun promptBoolean(text: String, default: Boolean?): Boolean { override fun render(template: String, ctx: Map<String, Any?>, output: String?) {
val suffix = when (default) { val renderedTemplate = pebble.getTemplate(template)(ctx)
true -> "[Y/n]" val outputPath = output?.let { outputPathRoot.resolve(it) } ?: outputPathRoot.resolve(template)
false -> "[y/N]"
null -> "[y/n]"
}
return this@GenerateCommand.prompt("$text $suffix", default = "$default", showDefault = false) {
when (it.toLowerCase()) {
"y" -> true
"n" -> false
else -> throw UsageError("Can only be [y/n]")
}
}!!
}
override fun promptInt(text: String, default: Int?): Int {
TODO("Not yet implemented")
}
override fun render(name: String, ctx: Map<String, Any?>) = pebble.getTemplate(name)(ctx)
override fun write(content: String, output: String) {
val outputPath = outputPathRoot.resolve(output)
Files.createDirectories(outputPath.parent) Files.createDirectories(outputPath.parent)
Files.writeString(outputPath, content) Files.writeString(outputPath, renderedTemplate)
} }
override fun copy(input: String, output: String) { override fun copy(input: String, output: String?) {
val inputPath = generator.treeRoot.resolve(input) val inputPath = generator.treeRoot.resolve(input)
val outputPath = outputPathRoot.resolve(output) val outputPath = output?.let { outputPathRoot.resolve(it) } ?: outputPathRoot.resolve(input)
Files.createDirectories(outputPath.parent) Files.createDirectories(outputPath.parent)
Files.copy(inputPath, outputPath) Files.copy(inputPath, outputPath)
} }
+26
View File
@@ -0,0 +1,26 @@
package scaffold.scripting
import org.graalvm.polyglot.Value
import org.graalvm.polyglot.proxy.ProxyExecutable
import org.graalvm.polyglot.proxy.ProxyObject
import scaffold.Prompt
class PromptProxyAdapter(private val prompt: Prompt) : ProxyObject {
override fun getMember(key: String?) = when (key) {
"string" -> ProxyExecutable { args ->
prompt.string(args[0].asString(), args.getOrNull(1)?.asString())
}
"boolean" -> ProxyExecutable { args ->
prompt.boolean(args[0].asString(), args.getOrNull(1)?.asBoolean())
}
else -> throw UnsupportedOperationException()
}
override fun getMemberKeys() = arrayOf("string", "boolean")
override fun hasMember(key: String?): Boolean = key in memberKeys
override fun putMember(key: String?, value: Value?) = throw UnsupportedOperationException()
}
+5 -6
View File
@@ -1,14 +1,13 @@
package scaffold.scripting package scaffold.scripting
import scaffold.Prompt
interface ScriptContext { interface ScriptContext {
fun echo(message: String) fun echo(message: String)
fun prompt(text: String, default: String?): String val prompt: Prompt
fun promptBoolean(text: String, default: Boolean?): Boolean
fun promptInt(text: String, default: Int?): Int
fun render(name: String, ctx: Map<String, Any?>): String fun render(template: String, ctx: Map<String, Any?>, output: String?)
fun write(content: String, output: String) fun copy(input: String, output: String?)
fun copy(input: String, output: String)
} }
+10 -20
View File
@@ -13,34 +13,24 @@ class ScriptEngine(private val scriptContext: ScriptContext) {
init { init {
with(context.getBindings("js")) { with(context.getBindings("js")) {
putMember("echo", ProxyExecutable { args -> putMember("echo", ProxyExecutable { (message) ->
scriptContext.echo(args[0].asString()) scriptContext.echo(message.asString())
null null
}) })
putMember("prompt", ProxyExecutable { args -> putMember("prompt", PromptProxyAdapter(scriptContext.prompt))
scriptContext.prompt(args[0].asString(), args.getOrNull(1)?.asString())
})
putMember("promptBoolean", ProxyExecutable { args ->
scriptContext.promptBoolean(args[0].asString(), args.getOrNull(1)?.asBoolean())
})
putMember("promptInt", ProxyExecutable { args ->
scriptContext.promptInt(args[0].asString(), args.getOrNull(1)?.asInt())
})
putMember("render", ProxyExecutable { args -> putMember("render", ProxyExecutable { args ->
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
scriptContext.render(args[0].asString(), args[1].`as`(Map::class.java) as Map<String, Any?>) scriptContext.render(
args[0].asString(),
args[1].`as`(Map::class.java) as Map<String, Any?>,
args.getOrNull(2)?.asString()
)
}) })
putMember("write", ProxyExecutable { (input, output) -> putMember("copy", ProxyExecutable { args ->
scriptContext.write(input.asString(), output.asString()) scriptContext.copy(args[0].asString(), args.getOrNull(1)?.asString())
})
putMember("copy", ProxyExecutable { (input, output) ->
scriptContext.copy(input.asString(), output.asString())
}) })
} }
} }
+60
View File
@@ -0,0 +1,60 @@
@file:Suppress("MemberVisibilityCanBePrivate")
package scaffold
import com.github.ajalt.clikt.output.CliktConsole
import io.mockk.clearMocks
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
import strikt.api.expectThat
import strikt.assertions.isEqualTo
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class PromptTest {
val console = mockk<CliktConsole>()
val prompt = Prompt(console)
@BeforeEach
fun beforeEach() = clearMocks(console)
@CsvSource(
value = [
"null, answer, answer",
"def, answer, answer",
"def, '', def"
]
)
@ParameterizedTest(name = "prompt string({argumentsWithNames})")
fun `prompt string`(default: String?, answer: String, expectedValue: String) {
every { console.promptForLine(any(), any()) } returns answer
expectThat(prompt.string("test", default)).isEqualTo(expectedValue)
}
@CsvSource(
value = [
"null, y, true",
"null, n, false",
"true, '', true",
"true, y, true",
"true, n, false",
"false, '', false",
"false, y, true",
"false, n, false",
]
)
@ParameterizedTest(name = "prompt boolean({argumentsWithNames})")
fun `prompt boolean`(default: Boolean?, answer: String, expectedValue: Boolean) {
every { console.promptForLine(any(), any()) } returns answer
expectThat(prompt.boolean("test", default)).isEqualTo(expectedValue)
}
}
+8 -24
View File
@@ -1,4 +1,4 @@
@file:Suppress("MemberVisibilityCanBePrivate", "PackageDirectoryMismatch") @file:Suppress("MemberVisibilityCanBePrivate")
package scaffold.scripting package scaffold.scripting
@@ -10,11 +10,15 @@ import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.TestInstance.Lifecycle import org.junit.jupiter.api.TestInstance.Lifecycle
import scaffold.Prompt
@TestInstance(Lifecycle.PER_CLASS) @TestInstance(Lifecycle.PER_CLASS)
class ScriptEngineTest { class ScriptEngineTest {
val scriptContext = mockk<ScriptContext>() val scriptContext = mockk<ScriptContext>().also {
every { it.prompt } returns Prompt()
}
val scriptEngine = ScriptEngine(scriptContext) val scriptEngine = ScriptEngine(scriptContext)
@BeforeEach @BeforeEach
@@ -32,7 +36,7 @@ class ScriptEngineTest {
@Test @Test
fun render() { fun render() {
every { scriptContext.render("test", match { it["something"] == "hello" }) } returns "blah" every { scriptContext.render("test", match { it["something"] == "hello" }, null) } returns Unit
val script = """ val script = """
const ctx = { const ctx = {
@@ -44,27 +48,7 @@ class ScriptEngineTest {
scriptEngine.eval(script) scriptEngine.eval(script)
verify { scriptContext.render(any(), any()) } verify { scriptContext.render(any(), any(), any()) }
}
@Test
fun prompt() {
every { scriptContext.prompt("a", null) } returns "a"
val script = "prompt('a')"
scriptEngine.eval(script)
verify { scriptContext.prompt(any(), null) }
}
@Test
fun promptWithDefault(){
every { scriptContext.prompt("a", "default") } returns "default"
val script = "prompt('a', 'default')"
scriptEngine.eval(script)
verify { scriptContext.prompt(any(), any()) }
} }
} }
+3 -6
View File
@@ -2,15 +2,12 @@ plugins {
`kotlin-dsl` `kotlin-dsl`
} }
kotlinDslPluginOptions {
experimentalWarning.set(false)
}
repositories { repositories {
gradlePluginPortal() gradlePluginPortal()
} }
dependencies { dependencies {
implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.4.31")) implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.21"))
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.31") implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.21")
implementation("com.github.johnrengelman:shadow:8.1.1")
} }
@@ -7,8 +7,9 @@ repositories {
} }
java { java {
targetCompatibility = JavaVersion.toVersion(11) toolchain {
sourceCompatibility = JavaVersion.toVersion(11) languageVersion.set(JavaLanguageVersion.of(19))
}
} }
tasks.withType<JavaCompile> { tasks.withType<JavaCompile> {
@@ -1,3 +1,5 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
@@ -18,9 +20,10 @@ tasks.withType<Test> {
} }
tasks.withType<KotlinCompile> { tasks.withType<KotlinCompile> {
kotlinOptions { compilerOptions {
jvmTarget = "11" jvmTarget.set(JvmTarget.JVM_19)
javaParameters = true javaParameters.set(true)
languageVersion.set(KotlinVersion.KOTLIN_2_0)
} }
} }
@@ -0,0 +1,54 @@
import org.gradle.jvm.toolchain.JavaToolchainService;
import java.io.ByteArrayOutputStream
task("buildNative") {
dependsOn("installShadowDist")
outputs.file("${buildDir}/native/scaffold")
doLast {
val graalvmHome = project.extensions.findByType<JavaToolchainService>()?.launcherFor {
languageVersion.set(JavaLanguageVersion.of(19))
vendor.set(JvmVendorSpec.GRAAL_VM)
}
?.orNull
?.executablePath?.asFile?.toPath()?.parent?.parent?.toString()
?: System.getenv("GRAALVM_HOME")
?: error("GRAALVM_HOME is not set")
val out = ByteArrayOutputStream()
exec {
commandLine(
"${graalvmHome}/bin/gu",
"list",
"-v",
)
standardOutput = out
}
val installedComponents = out.toString().lines()
.filter { it.startsWith("ID") }
.map { it.substringAfter(':').trim() }
if ("native-image" !in installedComponents) {
throw GradleException("GRAALVM: Missing js component")
}
if ("js" !in installedComponents) {
throw GradleException("GRAALVM: Missing js component")
}
exec {
commandLine(
"${graalvmHome}/bin/native-image",
"--no-fallback",
"-R:MaxNewSize=32",
"--language:js",
"-jar",
"${buildDir}/install/app-shadow/lib/scaffold.jar",
"${buildDir}/native/scaffold"
)
}
}
}
@@ -0,0 +1,7 @@
tasks.register<Zip>("release") {
dependsOn("buildNative")
archiveFileName.set("scaffold-${archiveVersion.get()}-linux.zip")
destinationDirectory.set(file("$buildDir/release"))
from("$buildDir/native/scaffold")
}
@@ -0,0 +1,11 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
plugins {
id("com.github.johnrengelman.shadow")
}
tasks.withType<ShadowJar> {
archiveBaseName.set("scaffold")
archiveClassifier.set("")
archiveVersion.set("")
}
+3
View File
@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8
org.gradle.caching=true
org.gradle.parallel=true
Binary file not shown.
+2 -1
View File
@@ -1,5 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists