Поскольку я работаю над перезагрузкой поддержки 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, поэтому я, вероятно, не буду тратить на это больше циклов. Всего за пару часов исследований и экспериментов я смог многому научиться и получить что-то действительно полезное, и уроки, которые я выучил, означают, что следующий из них, который я делаю, будет еще проще!