Поскольку я работаю над перезагрузкой поддержки JavaScript в Tapestry, одна из моих целей состоит в том, чтобы иметь возможность создавать JavaScript Tapestry как CoffeeScript. В конечном итоге у Tapestry будет опция динамической компиляции CoffeeScript в JavaScript, но я не хочу, чтобы это зависело от базовой библиотеки; это означает, что мне нужно, чтобы компиляция (транспиляция?) происходила во время сборки.
К счастью, это то, что Gradle делает хорошо! Моей отправной точкой для этого является проект « Оптимизатор веб-ресурсов для Java », который включает в себя много кода, часто использующего Rhino (для JavaScript) или JRuby, для выполнения ряда общих операций обработки веб-ресурсов, включая CoffeeScript для JavaScript. У WRO4J есть задача Ant, но я хотел создать что-то более идиоматическое для Gradle.
Вот что я придумала до сих пор. Это внешний скрипт сборки, отдельный от основного скрипта сборки проекта:
import ro.isdc.wro.model.resource.*
import ro.isdc.wro.extensions.processor.js.*
buildscript {
repositories { mavenCentral() }
dependencies {
classpath "ro.isdc.wro4j:wro4j-extensions:${versions.wro4j}"
}
}
class CompileCoffeeScript extends DefaultTask {
def srcDir = "src/main/coffeescript"
def outputDir = "${project.buildDir}/compiled-coffeescript"
@InputDirectory
File getSrcDir() { project.file(srcDir) }
@OutputDirectory
File getOutputDir() { project.file(outputDir) }
@TaskAction
void doCompile() {
logger.info "Compiling CoffeeScript sources from $srcDir into $outputDir"
def outputDirFile = getOutputDir()
// Recursively delete output directory if it exists
outputDirFile.deleteDir()
def tree = project.fileTree srcDir, {
include '**/*.coffee'
}
tree.visit { visit ->
if (visit.directory) return
def inputFile = visit.file
def inputPath = visit.path
def outputPath = inputPath.replaceAll(/\.coffee$/, '.js')
def outputFile = new File(outputDirFile, outputPath)
logger.info "Compiling ${inputPath}"
outputFile.parentFile.mkdirs()
def resource = Resource.create(inputFile.absolutePath, ResourceType.JS)
new CoffeeScriptProcessor().process(resource, inputFile.newReader(), outputFile.newWriter())
}
}
}
project.ext.CompileCoffeeScript = CompileCoffeeScript
Задача находит все файлы .coffee во входном каталоге (игнорируя все остальное) и генерирует соответствующий файл .js в выходном каталоге.
Аннотации @InputDirectory и @OutputDirectory позволяют Gradle решать, когда необходимо выполнить задачу: если какой-либо файл был изменен в каталогах, предоставленных этими методами, то задача должна быть перезапущена. Gradle не говорит нам, что именно изменилось, или создать каталоги, или что-нибудь … это зависит от нас.
Поскольку я рассчитываю получить только несколько файлов CoffeeScript, проще всего было просто удалить выходной каталог и перекомпилировать все входные файлы CoffeeScript при любом изменении. Аннотация @TaskAction указывает Gradle на вызов метода doCompile (), когда входы (или выходы) изменились со времени предыдущей сборки.
Интересно отметить, что посещение, переданное замыканию в строке 34, на самом деле является FileVisitDetails , что позволяет очень легко, помимо прочего, обрабатывать выходной файл на основе относительного пути от исходного каталога до входных данных. файл.
Одна из моих проблем была в том, чтобы настроить classpath для включения WRO4J и его зависимостей; Конфигурация buildscript в строке 4 специфична для этого единственного сценария сборки , что становится очевидным, как только вы его разобрались. На самом деле это отлично подходит для повторного использования, так как это означает, что необходимо внести минимальные изменения в build.gradle, который использует этот скрипт сборки. Ранее я ошибочно предполагал, что основной сценарий сборки должен был устанавливать путь к классам для любых внешних сценариев сборки.
Также обратите внимание на строку 54; задача CompileCoffeeScript должна быть экспортирована из этого сценария сборки в проект, чтобы проект мог фактически использовать ее.
Изменения в сценарии сборки проекта build.gradle достаточно малы:
apply from: "coffeescript.gradle"
task compileCoffeeScript(type: CompileCoffeeScript)
processResources {
from compileCoffeeScript
}
Применить из: вводит задачу CompileCoffeeScript. Затем мы используем этот класс для определения новой задачи, используя значения по умолчанию для srcDir и outputDir.
Последняя часть действительно интересна : мы берем существующую задачу processResources и добавляем в нее новый входной каталог … но вместо того, чтобы явно говорить о каталоге, мы просто поставляем задачу. Теперь Gradle знает, как сделать задачу compileCoffeeScript зависимой от задачи processResources, и добавить выходной каталог задачи (помните, что аннотация @OutputDirectory?) В качестве другого исходного каталога для processResources. Это означает, что Gradle будет легко перестраивать файлы JavaScript из файлов CoffeeScript, и эти файлы JavaScript будут затем включены в окончательный WAR или JAR (или в путь к классам при выполнении тестовых задач).
Обратите внимание на то, что мне не нужно было делать: мне не нужно было создавать плагин, или писать какой-либо XML, или загружать расширение Gradle в репозиторий, или даже придумать полное имя для расширения. Я только что написал файл и сослался на него из основного сценария сборки. Gradle позаботился обо всем остальном.
Здесь есть место для некоторого улучшения; Я подозреваю, что мог бы немного взломать, чтобы изменить способ сообщения об ошибках компиляции (сейчас это просто большой уродливый след стека). Я мог бы использовать пул потоков для параллельной компиляции. Я мог бы даже отказаться от подхода удаления-вывода-каталога-и-перекомпиляции-всего … но на данный момент то, что у меня есть, работает и достаточно быстро.
Люк Дейли сообщил, что в 1.1 появится экспериментальный плагин компиляции CoffeeScript, поэтому я, вероятно, не буду тратить на это больше циклов. Всего за пару часов исследований и экспериментов я смог многому научиться и получить что-то действительно полезное, и уроки, которые я выучил, означают, что следующий из них, который я делаю, будет еще проще!