diff --git a/.gitignore b/.gitignore index ae07d912..ac7a7af1 100644 --- a/.gitignore +++ b/.gitignore @@ -14,5 +14,3 @@ asset-pipeline-site/src/assets/html/apidoc/ .jruby-container .sass-cache .sass-work -node_modules/ -sass-dart-asset-pipeline/src/main/resources/js/ diff --git a/.sdkmanrc b/.sdkmanrc index 3f5e703c..80e8bbc0 100644 --- a/.sdkmanrc +++ b/.sdkmanrc @@ -1 +1,2 @@ gradle=6.5 +java=8.0.312-zulu diff --git a/asset-pipeline-core.ipr b/asset-pipeline-core.ipr index 2da04487..2775d189 100644 --- a/asset-pipeline-core.ipr +++ b/asset-pipeline-core.ipr @@ -357,14 +357,12 @@ - + - + - - - + @@ -1478,9 +1476,12 @@ + - + + + @@ -1566,9 +1567,12 @@ + - + + + @@ -1695,16 +1699,22 @@ + - + + + + - + + + @@ -2204,9 +2214,12 @@ + - + + + @@ -2794,9 +2807,12 @@ + - + + + @@ -2909,9 +2925,12 @@ + - + + + diff --git a/asset-pipeline-gradle/src/main/groovy/asset/pipeline/gradle/AssetPipelineExtension.groovy b/asset-pipeline-gradle/src/main/groovy/asset/pipeline/gradle/AssetPipelineExtension.groovy index 620071ed..5c2bd2ae 100644 --- a/asset-pipeline-gradle/src/main/groovy/asset/pipeline/gradle/AssetPipelineExtension.groovy +++ b/asset-pipeline-gradle/src/main/groovy/asset/pipeline/gradle/AssetPipelineExtension.groovy @@ -93,6 +93,6 @@ interface AssetPipelineExtension { @Input @Optional List getResolvers() - void setResolvers(List value) + void setResolvers(List value) } diff --git a/asset-pipeline-gradle/src/main/groovy/asset/pipeline/gradle/AssetPipelinePlugin.groovy b/asset-pipeline-gradle/src/main/groovy/asset/pipeline/gradle/AssetPipelinePlugin.groovy index d692a807..da4e05fe 100644 --- a/asset-pipeline-gradle/src/main/groovy/asset/pipeline/gradle/AssetPipelinePlugin.groovy +++ b/asset-pipeline-gradle/src/main/groovy/asset/pipeline/gradle/AssetPipelinePlugin.groovy @@ -48,7 +48,7 @@ class AssetPipelinePlugin implements Plugin { def defaultConfiguration = project.extensions.create('assets', AssetPipelineExtensionImpl) def config = AssetPipelineConfigHolder.config != null ? AssetPipelineConfigHolder.config : [:] - config.cacheLocation = "${project.buildDir}/.assCache" + config.cacheLocation = "${project.buildDir}/.assetcache" if (project.extensions.findByName('grails')) { defaultConfiguration.assetsPath = 'grails-app/assets' diff --git a/sass-dart-asset-pipeline/.gitignore b/sass-dart-asset-pipeline/.gitignore new file mode 100644 index 00000000..9a548f75 --- /dev/null +++ b/sass-dart-asset-pipeline/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +src/main/resources/js/ +javet-version.txt diff --git a/sass-dart-asset-pipeline/build.gradle b/sass-dart-asset-pipeline/build.gradle index a4fa9e93..b99626c8 100644 --- a/sass-dart-asset-pipeline/build.gradle +++ b/sass-dart-asset-pipeline/build.gradle @@ -13,11 +13,16 @@ apply plugin: 'groovy' apply plugin: 'maven-publish' apply plugin: 'java-library' apply plugin: 'idea' + group = 'com.bertramlabs.plugins' -ext.isReleaseVersion = !version.endsWith("SNAPSHOT") sourceCompatibility = '1.8' targetCompatibility = '1.8' +ext { + javetVersion = "1.0.6" + isReleaseVersion = !version.endsWith("SNAPSHOT") +} + repositories { mavenLocal() mavenCentral() @@ -45,10 +50,7 @@ apply plugin: 'com.github.node-gradle.node' dependencies { implementation 'org.codehaus.groovy:groovy-all:2.4.19' -// implementation 'com.eclipsesource.j2v8:j2v8:6.2.1' -// implementation 'com.eclipsesource.j2v8:j2v8_macosx_x86_64:4.6.0' -// implementation "com.caoccao.javet:javet:1.0.4" - implementation 'com.caoccao.javet:javet-macos:1.0.4' // Mac OS (x86_64 Only) + implementation "com.caoccao.javet:javet-core:${javetVersion}" api project(':asset-pipeline-core') api 'org.slf4j:slf4j-api:1.7.28' testImplementation 'org.codehaus.groovy:groovy-all:2.4.19' @@ -107,6 +109,14 @@ publishing { } +task copyJavetVersion() { + doLast { + new File("${projectDir}/src/main/resources", "javet-version.txt").text = javetVersion + } +} + +compileGroovy.dependsOn copyJavetVersion + task(console, dependsOn: 'classes', type: JavaExec) { main = 'groovy.ui.Console' classpath = sourceSets.main.runtimeClasspath @@ -121,5 +131,5 @@ test { node { download = true - version = "14.18.1" + version = "16.13.0" } diff --git a/sass-dart-asset-pipeline/src/main/groovy/asset/pipeline/dart/NativeLibraryUtil.groovy b/sass-dart-asset-pipeline/src/main/groovy/asset/pipeline/dart/NativeLibraryUtil.groovy new file mode 100644 index 00000000..2d762a31 --- /dev/null +++ b/sass-dart-asset-pipeline/src/main/groovy/asset/pipeline/dart/NativeLibraryUtil.groovy @@ -0,0 +1,106 @@ +package asset.pipeline.dart + +import asset.pipeline.AssetPipelineConfigHolder +import com.caoccao.javet.enums.JSRuntimeType +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j + +import java.nio.channels.Channels +import java.nio.channels.FileChannel +import java.nio.channels.ReadableByteChannel +import java.nio.file.Files +import java.nio.file.Paths +import java.nio.file.StandardCopyOption +import java.util.jar.JarEntry +import java.util.jar.JarFile + +@Slf4j +@CompileStatic +class NativeLibraryUtil { + static final String JAVET_VERSION = NativeLibraryUtil.class.classLoader.getResource("javet-version.txt").openStream().text + static final String BASE_URL = AssetPipelineConfigHolder.config?.javetBaseUrl ?: "https://repo1.maven.org/maven2/com/caoccao/javet/javet-macos/${JAVET_VERSION}/" + + static final String TMP_DIR = System.getProperty("java.io.tmpdir") + static final String OS_ARCH = System.getProperty("os.arch") + static final String OS_NAME = System.getProperty("os.name") + + static final boolean IS_LINUX = OS_NAME.startsWith("Linux") + static final boolean IS_MACOS = OS_NAME.startsWith("Mac OS") + static final boolean IS_WINDOWS = OS_NAME.startsWith("Windows") + static final boolean IS_ARM64 = OS_ARCH.startsWith("arm64") || OS_ARCH.startsWith("armv8") || OS_ARCH == "aarch64" + static final boolean IS_X86_64 = OS_ARCH.matches(/^(x8664|amd64|ia32e|em64t|x64|x86_64)$/) + + static final String ARCH_NAME = IS_ARM64 ? 'arm64' : 'x86_64' + + static URL getJavetJARUrl() { + new URL(BASE_URL + getJavetFileName()) + } + + static String getJavetFileName() { + IS_MACOS ? "javet-macos-${JAVET_VERSION}.jar" : "javet-${JAVET_VERSION}.jar" + } + + static String getLibraryName(JSRuntimeType runtimeType) { + if (IS_MACOS) { + return "libjavet-${runtimeType.name}-macos-${ARCH_NAME}.v.${JAVET_VERSION}.dylib" + } + if (IS_LINUX) { + return "libjavet-${runtimeType.name}-linux-${ARCH_NAME}.v.${JAVET_VERSION}.so" + } + if (IS_WINDOWS) { + return "libjavet-${runtimeType.name}-windows-${ARCH_NAME}.v.${JAVET_VERSION}.dll" + } + + throw new IllegalStateException("Platform type not supported: ${OS_NAME} (${OS_ARCH}), expected Mac OS, Linux, or Windows") + } + + static File downloadJavetJar(JSRuntimeType runtimeType) { + File jarFile = new File(TMP_DIR, javetFileName) + if (jarFile.exists()) { + log.debug("Jar file ${jarFile.path} exists, skipping download") + return jarFile + } + + // Download the file + URL jarUrl = getJavetJARUrl() + log.info("Downloading sass-dart native library integration: ${jarUrl}") + + copyToFile(jarUrl.openStream(), jarFile) + + log.debug("Downloaded $jarUrl to $jarFile (${jarFile.size()} bytes)") + jarFile + } + + static File extractNativeLibrary(JSRuntimeType runtimeType) { + File file = downloadJavetJar(runtimeType) + if (!file.exists()) { + throw new IllegalStateException("Javet jar file $file does not exist, perhaps the download failed") + } + + String libraryName = getLibraryName(runtimeType) + File jniLibrary = new File(TMP_DIR, libraryName) + if (jniLibrary.exists()) { + log.debug("Native library $libraryName already exists, skipping extract") + return jniLibrary + } + + // Extract from the JAR file, should be in the root + JarFile jarFile = new JarFile(file) + JarEntry jarEntry = jarFile.getJarEntry(libraryName) + if (!jarEntry) { + throw new IllegalStateException("Could not load native library: $libraryName") + } + + copyToFile(jarFile.getInputStream(jarEntry), jniLibrary) + + log.debug("Extracted native library $libraryName to ${jniLibrary.path}") + jniLibrary + } + + static private void copyToFile(InputStream inputStream, File targetFile) { + ReadableByteChannel readableByteChannel = Channels.newChannel(inputStream) + FileOutputStream fileOutputStream = new FileOutputStream(targetFile) + FileChannel fileChannel = fileOutputStream.getChannel() + fileChannel.transferFrom(readableByteChannel, 0, Long.MAX_VALUE) + } +} diff --git a/sass-dart-asset-pipeline/src/main/groovy/asset/pipeline/dart/SassAssetFileLoader.groovy b/sass-dart-asset-pipeline/src/main/groovy/asset/pipeline/dart/SassAssetFileLoader.groovy index 0c1fdf73..04f7e4aa 100644 --- a/sass-dart-asset-pipeline/src/main/groovy/asset/pipeline/dart/SassAssetFileLoader.groovy +++ b/sass-dart-asset-pipeline/src/main/groovy/asset/pipeline/dart/SassAssetFileLoader.groovy @@ -2,6 +2,7 @@ package asset.pipeline.dart import asset.pipeline.AssetFile import asset.pipeline.AssetHelper +import asset.pipeline.CacheManager import com.caoccao.javet.annotations.V8Function import groovy.transform.CompileStatic import groovy.util.logging.Slf4j @@ -30,19 +31,17 @@ class SassAssetFileLoader { * * @param url the import it appears in the source file * @prev either 'stdin' for the first level imports or the original url from the parent for nested - * @param assetFilePath the original assetFile.path that started the import chain * @return https://sass-lang.com/documentation/js-api/modules#LegacyImporterResult */ @V8Function @SuppressWarnings('unused') - Map resolveImport(String url, String prev, String assetFilePath) { - log.debug("Resolving import for url [{}], prev [{}], asset file path [{}]", url, prev, assetFilePath) - println " > Importing [${url}], prev [$prev], asset file [${assetFilePath}]" + Map resolveImport(String url, String prev) { + log.debug("Importing for url [{}], prev [{}], base file [{}]", url, prev, baseFile?.path) // The initial import has a path of stdin, but we need to convert that to the proper base path // Otherwise, if we have a parent, append that to form the correct URL as the importer syntax doesn't send what's expected if (prev == 'stdin') { - prev = assetFilePath + prev = baseFile.path } else { // Resolve the real base path for this import @@ -60,6 +59,8 @@ class SassAssetFileLoader { importMap[url] = prev AssetFile imported = getAssetFromScssImport(prev, url) + CacheManager.addCacheDependency(baseFile.path, imported) + return [contents: imported.inputStream.text] } diff --git a/sass-dart-asset-pipeline/src/main/groovy/asset/pipeline/dart/SassProcessor.groovy b/sass-dart-asset-pipeline/src/main/groovy/asset/pipeline/dart/SassProcessor.groovy index ae796d9f..68170e1d 100644 --- a/sass-dart-asset-pipeline/src/main/groovy/asset/pipeline/dart/SassProcessor.groovy +++ b/sass-dart-asset-pipeline/src/main/groovy/asset/pipeline/dart/SassProcessor.groovy @@ -19,11 +19,10 @@ import asset.pipeline.AbstractProcessor import asset.pipeline.AssetCompiler import asset.pipeline.AssetFile import com.caoccao.javet.enums.JSRuntimeType -import com.caoccao.javet.interception.logging.JavetStandardConsoleInterceptor -import com.caoccao.javet.interop.V8Runtime -import com.caoccao.javet.interop.engine.IJavetEngine -import com.caoccao.javet.interop.engine.IJavetEnginePool -import com.caoccao.javet.interop.engine.JavetEnginePool +import com.caoccao.javet.interop.NodeRuntime +import com.caoccao.javet.interop.V8Host +import com.caoccao.javet.interop.loader.IJavetLibLoadingListener +import com.caoccao.javet.interop.loader.JavetLibLoader import com.caoccao.javet.values.reference.V8ValueObject import groovy.transform.CompileStatic import groovy.util.logging.Slf4j @@ -31,20 +30,31 @@ import groovy.util.logging.Slf4j @Slf4j @CompileStatic class SassProcessor extends AbstractProcessor { - final IJavetEnginePool javetEnginePool final String sassCompiler + static { + File nativeLibrary = NativeLibraryUtil.extractNativeLibrary(JSRuntimeType.Node) + + // Override Javet to use the library that we downloaded for this platform + JavetLibLoader.setLibLoadingListener(new IJavetLibLoadingListener() { + @Override + File getLibPath(JSRuntimeType jsRuntimeType) { + return nativeLibrary.parentFile + } + + @Override + boolean isDeploy(JSRuntimeType jsRuntimeType) { + return false + } + }) + } + SassProcessor(AssetCompiler precompiler) { super(precompiler) // Load script from classpath URL resource = getClass().classLoader.getResource("js/compiler.js") sassCompiler = resource.openStream().text - - // Setup a Javet engine for pooling - javetEnginePool = new JavetEnginePool<>() - javetEnginePool.getConfig().setJSRuntimeType(JSRuntimeType.Node) - javetEnginePool.getConfig().setAllowEval(true) } /** @@ -55,23 +65,19 @@ class SassProcessor extends AbstractProcessor { */ String process(String input, AssetFile assetFile) { log.debug "Compiling $assetFile.path" - println "Compiling $assetFile.path" String output = null - IJavetEngine javetEngine = javetEnginePool.getEngine() + NodeRuntime nodeRuntime = V8Host.getNodeInstance().createV8Runtime(true, JSRuntimeType.Node.runtimeOptions) as NodeRuntime try { - V8Runtime v8Runtime = javetEngine.getV8Runtime() - - // Create a Javet console interceptor. - JavetStandardConsoleInterceptor javetConsoleInterceptor = new JavetStandardConsoleInterceptor(v8Runtime) - javetConsoleInterceptor.register(v8Runtime.getGlobalObject()) + nodeRuntime.allowEval(true) // Bind the importer callback SassAssetFileLoader loader = new SassAssetFileLoader(assetFile) - V8ValueObject v8ValueObject = v8Runtime.createV8ValueObject() + + V8ValueObject v8ValueObject = nodeRuntime.createV8ValueObject() try { - v8Runtime.getGlobalObject().set("importer", v8ValueObject) + nodeRuntime.getGlobalObject().set("importer", v8ValueObject) v8ValueObject.bind(loader) } finally { @@ -80,21 +86,17 @@ class SassProcessor extends AbstractProcessor { // Setup the options passed to the SASS compiler // https://sass-lang.com/documentation/js-api/interfaces/LegacyStringOptions - v8Runtime.getGlobalObject().setProperty("compileOptions", [ - assetFilePath: assetFile.path, - data: input, - ]) + nodeRuntime.getGlobalObject().setProperty("compileOptions", [data: input]) // Compile and retrieve the CSS output - v8Runtime.getExecutor(sassCompiler).executeVoid() - output = v8Runtime.getGlobalObject().get("css") as String + nodeRuntime.getExecutor(sassCompiler).executeVoid() + output = nodeRuntime.getGlobalObject().get("css") as String - // Unregister the Javet console to V8 global object. - javetConsoleInterceptor.unregister(v8Runtime.getGlobalObject()) - v8Runtime.lowMemoryNotification() + // Cleanup the global importer + nodeRuntime.getGlobalObject().delete("importer") } finally { - javetEngine.close() + nodeRuntime.close() } output diff --git a/sass-dart-asset-pipeline/src/main/js/compiler.js b/sass-dart-asset-pipeline/src/main/js/compiler.js index 8ea40daf..1164ae87 100644 --- a/sass-dart-asset-pipeline/src/main/js/compiler.js +++ b/sass-dart-asset-pipeline/src/main/js/compiler.js @@ -1,11 +1,7 @@ const sass = require('sass'); // Call back for resolving imports via the Java asset pipeline resolvers -compileOptions.importer = [ - function(url, prev) { - return importer.resolveImport(url, prev, compileOptions.assetFilePath); - } -]; +compileOptions.importer = [importer.resolveImport] // Compile and return the rendered CSS result = sass.renderSync(compileOptions); diff --git a/sass-dart-asset-pipeline/src/test/groovy/asset/pipeline/dart/SassProcessorSpec.groovy b/sass-dart-asset-pipeline/src/test/groovy/asset/pipeline/dart/SassProcessorSpec.groovy index ccaf1b7c..c4f9b4e2 100644 --- a/sass-dart-asset-pipeline/src/test/groovy/asset/pipeline/dart/SassProcessorSpec.groovy +++ b/sass-dart-asset-pipeline/src/test/groovy/asset/pipeline/dart/SassProcessorSpec.groovy @@ -16,9 +16,10 @@ package asset.pipeline.dart +import asset.pipeline.AssetHelper +import asset.pipeline.AssetPipelineConfigHolder +import asset.pipeline.fs.FileSystemAssetResolver import spock.lang.Specification -import asset.pipeline.fs.* -import asset.pipeline.* /** * @author David Estes @@ -34,7 +35,6 @@ class SassProcessorSpec extends Specification { def processor = new SassProcessor() when: def output = processor.process(assetFile.inputStream.text,assetFile) - println "Results \n ${output}" then: output.contains('margin') } @@ -48,7 +48,6 @@ class SassProcessorSpec extends Specification { def processor = new SassProcessor() when: def output = processor.process(assetFile.inputStream.text,assetFile) - println "Results \n ${output}" then: output.contains('.sub') } @@ -62,7 +61,6 @@ class SassProcessorSpec extends Specification { def processor = new SassProcessor() when: def output = processor.process(assetFile.inputStream.text,assetFile) - println "Results \n ${output}" then: output.contains('.bar') } @@ -76,7 +74,6 @@ class SassProcessorSpec extends Specification { def processor = new SassProcessor() when: def output = processor.process(assetFile.inputStream.text, assetFile) - println "Results \n ${output}" then: output.length() > 0 } @@ -90,7 +87,6 @@ class SassProcessorSpec extends Specification { def processor = new SassProcessor() when: def output = processor.process(assetFile.inputStream.text, assetFile) - println "Results \n ${output}" then: output.contains('Twitter') }