Initial commit

This commit is contained in:
2021-04-01 23:18:28 +02:00
commit 2bd6362268
27 changed files with 732 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
plugins {
id("kotlin-application")
id("com.github.johnrengelman.shadow") version "6.1.0"
}
dependencies {
implementation("org.slf4j:slf4j-api:2.0.0-alpha1")
runtimeOnly("org.slf4j:slf4j-simple:2.0.0-alpha1")
implementation("io.pebbletemplates:pebble:3.1.5")
implementation("com.github.ajalt.clikt:clikt:3.1.0")
implementation("com.electronwill.night-config:toml:3.6.3")
}
application {
mainClassName = "scaffold.ScaffoldKt"
mainClass.set("scaffold.ScaffoldKt")
applicationName = "scaffold"
}
tasks.withType<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar> {
archiveBaseName.set("scaffold")
archiveClassifier.set("")
archiveVersion.set("")
}
+46
View File
@@ -0,0 +1,46 @@
package scaffold
import com.electronwill.nightconfig.core.Config
import com.electronwill.nightconfig.core.UnmodifiableConfig
import com.electronwill.nightconfig.core.file.FileConfig
import java.nio.file.Path
data class GeneratorConfig(
val templates: List<String>,
val files: List<Path>,
val prompts: Map<String, List<PromptValue>>
)
fun Generator.loadConfig(): GeneratorConfig {
val config: UnmodifiableConfig = FileConfig.of(rootPath.resolve("config.toml")).apply { load() }
val templates = config.get<List<String>>("templates")
val prompts = mutableMapOf<String, MutableList<PromptValue>>()
val section = config.get<Any?>("prompt") ?: error("Missing prompt section")
if (section !is Config) error("Prompt section is not an object")
for ((subsectionName, subsection) in section.valueMap()) {
check(subsection is List<*>) { "prompts.$subsectionName is not a list of prompts" }
for (prompt in subsection) {
check(prompt is Config)
val promptValue = PromptValue(
name = prompt.get("name") ?: error("Missing name"),
info = prompt.get("info"),
type = prompt.getEnum("type", PromptType::class.java) ?: PromptType.String
)
val current = prompts.computeIfAbsent(subsectionName) { ArrayList() }
current += promptValue
}
}
val files = config.get<List<String>>("files").flatMap { path ->
if ("*" in path) FileGlob.listFiles(treeRoot, path)
else listOf(treeRoot.resolve(path))
}
return GeneratorConfig(templates, files, prompts)
}
+22
View File
@@ -0,0 +1,22 @@
package scaffold
import java.nio.file.*
import java.nio.file.attribute.BasicFileAttributes
object FileGlob {
fun listFiles(root: Path, glob: String): MutableList<Path> {
val matcher = FileSystems.getDefault().getPathMatcher("glob:$root/$glob")
val matches = mutableListOf<Path>()
Files.walkFileTree(root, object : SimpleFileVisitor<Path>() {
override fun visitFile(file: Path, attrs: BasicFileAttributes?): FileVisitResult {
if (matcher.matches(file)) matches.add(file)
return FileVisitResult.CONTINUE
}
})
return matches
}
}
+8
View File
@@ -0,0 +1,8 @@
package scaffold
import java.nio.file.Path
class Generator(configDirectory: Path, name: String) {
val rootPath: Path = configDirectory.resolve(name)
val treeRoot: Path = rootPath.resolve("tree")
}
+17
View File
@@ -0,0 +1,17 @@
package scaffold
import java.nio.file.Files
import java.nio.file.Path
import kotlin.streams.toList
class Generators(val configDirectory: Path) {
fun isValid(name: String): Boolean {
val generatorRoot = configDirectory.resolve(name)
return Files.isDirectory(generatorRoot) && Files.isRegularFile(generatorRoot.resolve("config.toml"))
}
fun findAll(): List<Path> = Files
.list(configDirectory)
.filter { Files.exists(it.resolve("config.toml")) }
.toList()
}
+9
View File
@@ -0,0 +1,9 @@
package scaffold
enum class PromptType { String }
data class PromptValue(
val name: String,
val info: String?,
val type: PromptType,
)
+29
View File
@@ -0,0 +1,29 @@
package scaffold
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.subcommands
import scaffold.commands.GenerateCommand
import scaffold.commands.ListCommand
import scaffold.commands.NewCommand
import java.nio.file.Path
class Scaffold : CliktCommand() {
override fun run() {}
}
fun main(args: Array<String>) {
val configDirectory = when (System.getProperty("os.name")) {
"Linux" -> System.getenv("XDG_CONFIG_HOME")?.let { Path.of(it) }
?: Path.of(System.getProperty("user.home"), ".config")
else -> TODO("¯\\_(ツ)_/¯")
}
val scaffoldConfigDirectory = configDirectory.resolve("scaffold")
val generatorService = Generators(scaffoldConfigDirectory)
Scaffold().subcommands(
ListCommand(generatorService),
GenerateCommand(generatorService),
NewCommand(generatorService)
).main(args)
}
+84
View File
@@ -0,0 +1,84 @@
package scaffold.commands
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.ProgramResult
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.options.convert
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.required
import com.mitchellbosecke.pebble.PebbleEngine
import com.mitchellbosecke.pebble.loader.FileLoader
import com.mitchellbosecke.pebble.template.PebbleTemplate
import scaffold.Generator
import scaffold.Generators
import scaffold.PromptType
import scaffold.loadConfig
import java.io.StringWriter
import java.nio.file.Files
import java.nio.file.Path
class GenerateCommand(private val generators: Generators) : CliktCommand("generate") {
private val name by argument(help = "generator name")
private val output by option().convert { Path.of(it) }.required()
override fun run() {
checkPreconditions()
val generator = Generator(generators.configDirectory, name)
val config = generator.loadConfig()
val templateContext = mutableMapOf<String, Any?>()
for ((path, prompts) in config.prompts) {
val map = mutableMapOf<String, Any>()
templateContext[path] = map
for ((name, info, type) in prompts) {
if (type != PromptType.String) TODO()
val value = prompt(info ?: name)!!
map[name] = value
}
}
val pebble = PebbleEngine.Builder()
.autoEscaping(false)
.newLineTrimming(false)
.loader(FileLoader().apply { prefix = generator.treeRoot.toString() })
.build()!!
for (template in config.templates) {
val renderedTemplate = pebble.getTemplate(template)(templateContext)
val outputPath = output.resolve(template)
Files.createDirectories(outputPath.parent)
Files.writeString(outputPath, renderedTemplate)
}
for (inputFile in config.files) {
val relativeFile = generator.treeRoot.relativize(inputFile)
val outputFile = output.resolve(relativeFile)
Files.createDirectories(outputFile.parent)
Files.copy(inputFile, outputFile)
}
echo("Generated project in $output")
}
private fun checkPreconditions() {
if (Files.exists(output)) {
if (Files.list(output).findAny().isPresent) {
echo("Output path `$output` is not empty")
throw ProgramResult(1)
}
}
if (!generators.isValid(name)) {
echo("Generator not found")
throw ProgramResult(1)
}
}
}
operator fun PebbleTemplate.invoke(context: Map<String, Any?>): String =
StringWriter().also { evaluate(it, context) }.toString()
+8
View File
@@ -0,0 +1,8 @@
package scaffold.commands
import com.github.ajalt.clikt.core.CliktCommand
import scaffold.Generators
class ListCommand(private val generators: Generators) : CliktCommand("list") {
override fun run() = echo(generators.findAll().joinToString(" ") { it.last().toString() })
}
+48
View File
@@ -0,0 +1,48 @@
package scaffold.commands
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.ProgramResult
import com.github.ajalt.clikt.parameters.arguments.argument
import scaffold.Generators
import java.nio.file.Files
class NewCommand(private val generators: Generators) : CliktCommand("new") {
private val name by argument()
override fun run() {
val root = generators.configDirectory.resolve(name)
if (Files.exists(root)) {
echo("Generator already exists")
throw ProgramResult(1)
}
Files.createDirectories(root)
val configFile = """
templates = [
"README.md"
]
files = [
".gitignore"
]
[prompt]
[[prompt.project]]
type = "String"
name = "name"
info = "Project name"
""".trimIndent()
Files.writeString(root.resolve("config.toml"), configFile)
val tree = root.resolve("tree")
Files.createDirectory(tree)
Files.writeString(tree.resolve("README.md"), "# {{ project.name }}")
Files.writeString(tree.resolve(".gitignore"), "# https://git-scm.com/docs/gitignore")
echo("Generator created in $root")
}
}