Статьи

Gradle CoffeeScript Компиляция

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