From ba821b11c2b3cfa25982caaf56eb2ccc8e0bf390 Mon Sep 17 00:00:00 2001 From: Jesse Brault Date: Sun, 4 Jan 2026 11:43:18 -0600 Subject: [PATCH] Overhaul for 0.7.0-SNAPSHOT. --- README.md | 3 +- .../ssg/ComponentClassScanner.java | 10 ++ .../jessebrault/ssg/ConsolePageWriter.java | 15 ++ .../ssg/DefaultComponentClassScanner.java | 41 +++++ .../ssg/DefaultObjectFactoryConfigurator.java | 57 +++++++ .../ssg/DefaultPageContextFactory.groovy | 68 +++++++++ .../jessebrault/ssg/DefaultPageRenderer.java | 62 ++++++++ .../jessebrault/ssg/DefaultPageScanner.java | 80 ++++++++++ .../ssg/DefaultStaticSiteGenerator.groovy | 2 - .../jessebrault/ssg/DefaultTextsGetter.java | 25 ++++ .../ssg/DefaultWvcCompilerFactory.java | 22 +++ .../com/jessebrault/ssg/FilePageWriter.java | 43 ++++++ .../ssg/JDefaultStaticSiteGenerator.java | 140 ++++++++++++++++++ .../ssg/ObjectFactoryConfigurator.java | 8 + .../jessebrault/ssg/PageContextFactory.java | 16 ++ .../com/jessebrault/ssg/PageRenderer.java | 18 +++ .../com/jessebrault/ssg/PageScanner.java | 11 ++ .../com/jessebrault/ssg/PageWriter.java | 9 ++ .../jessebrault/ssg/StaticSiteGenerator.java | 13 +- .../com/jessebrault/ssg/TextsGetter.java | 10 ++ .../jessebrault/ssg/WvcCompilerFactory.java | 7 + .../jessebrault/ssg/WvcContextFactory.java | 8 + .../ssg/buildscript/BuildScriptBase.groovy | 20 +++ .../ssg/buildscript/BuildScriptFactory.java | 7 + .../BuildScriptToBuildSpecConverter.groovy | 3 +- .../ssg/buildscript/BuildSpec.groovy | 20 ++- .../ssg/buildscript/BuildSpecFactory.java | 7 + .../DefaultBuildScriptFactory.java | 78 ++++++++++ .../buildscript/DefaultBuildSpecFactory.java | 64 ++++++++ .../delegates/BuildDelegate.groovy | 51 ++----- .../delegates/BuildDelegateConfigurator.java | 5 + .../delegates/BuildDelegateConverter.java | 7 + .../DefaultBuildDelegateConfigurator.groovy | 39 +++++ .../DefaultBuildDelegateConverter.groovy | 22 +++ .../jessebrault/ssg/text/TextSupplier.java | 8 + .../text/TextsDirMarkdownTextSupplier.java | 58 ++++++++ .../jessebrault/ssg/util/Diagnostic.groovy | 6 +- .../jessebrault/ssg/view/WvcCompiler.groovy | 12 +- buildSrc/src/main/groovy/ssg-common.gradle | 2 +- .../ssg/AbstractBuildCommand.groovy | 71 +++++++-- .../ssg/StaticSiteGeneratorCli.groovy | 2 +- gradle/libs.versions.toml | 2 +- .../ssg/gradle/SsgGradlePlugin.java | 4 +- 43 files changed, 1074 insertions(+), 82 deletions(-) create mode 100644 api/src/main/groovy/com/jessebrault/ssg/ComponentClassScanner.java create mode 100644 api/src/main/groovy/com/jessebrault/ssg/ConsolePageWriter.java create mode 100644 api/src/main/groovy/com/jessebrault/ssg/DefaultComponentClassScanner.java create mode 100644 api/src/main/groovy/com/jessebrault/ssg/DefaultObjectFactoryConfigurator.java create mode 100644 api/src/main/groovy/com/jessebrault/ssg/DefaultPageContextFactory.groovy create mode 100644 api/src/main/groovy/com/jessebrault/ssg/DefaultPageRenderer.java create mode 100644 api/src/main/groovy/com/jessebrault/ssg/DefaultPageScanner.java create mode 100644 api/src/main/groovy/com/jessebrault/ssg/DefaultTextsGetter.java create mode 100644 api/src/main/groovy/com/jessebrault/ssg/DefaultWvcCompilerFactory.java create mode 100644 api/src/main/groovy/com/jessebrault/ssg/FilePageWriter.java create mode 100644 api/src/main/groovy/com/jessebrault/ssg/JDefaultStaticSiteGenerator.java create mode 100644 api/src/main/groovy/com/jessebrault/ssg/ObjectFactoryConfigurator.java create mode 100644 api/src/main/groovy/com/jessebrault/ssg/PageContextFactory.java create mode 100644 api/src/main/groovy/com/jessebrault/ssg/PageRenderer.java create mode 100644 api/src/main/groovy/com/jessebrault/ssg/PageScanner.java create mode 100644 api/src/main/groovy/com/jessebrault/ssg/PageWriter.java create mode 100644 api/src/main/groovy/com/jessebrault/ssg/TextsGetter.java create mode 100644 api/src/main/groovy/com/jessebrault/ssg/WvcCompilerFactory.java create mode 100644 api/src/main/groovy/com/jessebrault/ssg/WvcContextFactory.java create mode 100644 api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildScriptFactory.java create mode 100644 api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildSpecFactory.java create mode 100644 api/src/main/groovy/com/jessebrault/ssg/buildscript/DefaultBuildScriptFactory.java create mode 100644 api/src/main/groovy/com/jessebrault/ssg/buildscript/DefaultBuildSpecFactory.java create mode 100644 api/src/main/groovy/com/jessebrault/ssg/buildscript/delegates/BuildDelegateConfigurator.java create mode 100644 api/src/main/groovy/com/jessebrault/ssg/buildscript/delegates/BuildDelegateConverter.java create mode 100644 api/src/main/groovy/com/jessebrault/ssg/buildscript/delegates/DefaultBuildDelegateConfigurator.groovy create mode 100644 api/src/main/groovy/com/jessebrault/ssg/buildscript/delegates/DefaultBuildDelegateConverter.groovy create mode 100644 api/src/main/groovy/com/jessebrault/ssg/text/TextSupplier.java create mode 100644 api/src/main/groovy/com/jessebrault/ssg/text/TextsDirMarkdownTextSupplier.java diff --git a/README.md b/README.md index 8931cae..f2b0001 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,8 @@ updated to the same version in `cli/build.gradle`. ## Version-bumping Update the version of the project in `buildSrc/src/main/groovy/ssg-common.gradle`. Then update the references to the -`cli` and `api` projects in `ssg-gradle-plugin/src/main/java/com/jessebrault/ssg/gradle/SsgGradlePlugin.java`. +`cli` and `api` projects in `ssg-gradle-plugin/src/main/java/com/jessebrault/ssg/gradle/SsgGradlePlugin.java`. Finally, +update the version in the `cli` project for the cli info message. ## Publishing diff --git a/api/src/main/groovy/com/jessebrault/ssg/ComponentClassScanner.java b/api/src/main/groovy/com/jessebrault/ssg/ComponentClassScanner.java new file mode 100644 index 0000000..9a7a364 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/ComponentClassScanner.java @@ -0,0 +1,10 @@ +package com.jessebrault.ssg; + +import groowt.view.component.web.WebViewComponent; +import io.github.classgraph.ScanResult; + +import java.util.Set; + +public interface ComponentClassScanner { + Set> getWebViewComponentClasses(ScanResult scanResult); +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/ConsolePageWriter.java b/api/src/main/groovy/com/jessebrault/ssg/ConsolePageWriter.java new file mode 100644 index 0000000..5ab7d1a --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/ConsolePageWriter.java @@ -0,0 +1,15 @@ +package com.jessebrault.ssg; + +import com.jessebrault.ssg.page.Page; + +import java.io.File; + +public class ConsolePageWriter implements PageWriter { + + @Override + public void write(Page page, File outputDir, String renderedPage) { + System.out.println("--- Page " + page.getPath() + " ---"); + System.out.println(renderedPage); + } + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/DefaultComponentClassScanner.java b/api/src/main/groovy/com/jessebrault/ssg/DefaultComponentClassScanner.java new file mode 100644 index 0000000..9017e04 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/DefaultComponentClassScanner.java @@ -0,0 +1,41 @@ +package com.jessebrault.ssg; + +import groowt.view.component.web.WebViewComponent; +import io.github.classgraph.ClassInfoList; +import io.github.classgraph.ScanResult; +import jakarta.inject.Inject; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; + +public class DefaultComponentClassScanner implements ComponentClassScanner { + + private final ExecutorService executorService; + + @Inject + public DefaultComponentClassScanner(ExecutorService executorService) { + this.executorService = executorService; + } + + @Override + public Set> getWebViewComponentClasses(ScanResult scanResult) { + final ClassInfoList classInfoList = scanResult.getClassesImplementing(WebViewComponent.class); + final Set> results = ConcurrentHashMap.newKeySet(); + + // fork + final List> futures = classInfoList.stream().map(classInfo -> + CompletableFuture.runAsync(() -> + results.add(classInfo.loadClass(WebViewComponent.class)), + executorService + )).toList(); + + // join + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + + return results; + } + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/DefaultObjectFactoryConfigurator.java b/api/src/main/groovy/com/jessebrault/ssg/DefaultObjectFactoryConfigurator.java new file mode 100644 index 0000000..311b8c2 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/DefaultObjectFactoryConfigurator.java @@ -0,0 +1,57 @@ +package com.jessebrault.ssg; + +import com.jessebrault.di.RegistryObjectFactory; +import com.jessebrault.ssg.buildscript.BuildSpec; +import com.jessebrault.ssg.di.GlobalsExtension; +import com.jessebrault.ssg.di.ModelsExtension; +import com.jessebrault.ssg.di.TextsExtension; +import jakarta.inject.Inject; + +import static com.jessebrault.di.BindingUtil.named; +import static com.jessebrault.di.BindingUtil.toSingleton; + +public class DefaultObjectFactoryConfigurator implements ObjectFactoryConfigurator { + + private final TextsGetter textsGetter; + + @Inject + public DefaultObjectFactoryConfigurator(TextsGetter textsGetter) { + this.textsGetter = textsGetter; + } + + @Override + public void configure(RegistryObjectFactory registryObjectFactory, BuildSpec buildSpec) { + registryObjectFactory.configureRegistry(registry -> { + // texts + final var textsExtension = new TextsExtension(); + textsExtension.getAllTexts().addAll(this.textsGetter.getTexts(buildSpec)); + registry.addExtension(textsExtension); + + // models + final var modelsExtension = new ModelsExtension(); + modelsExtension.getAllModels().addAll(buildSpec.getModels().get(() -> + new SsgException("the models Property in " + buildSpec.getName() + + " must contain at least an empty set.") + )); + registry.addExtension(modelsExtension); + + // globals + final var globalsExtension = new GlobalsExtension(); + globalsExtension.getGlobals().putAll(buildSpec.getGlobals().get(() -> + new SsgException("the globals Property in " + buildSpec.getName() + + " must contain at least an empty set.") + )); + registry.addExtension(globalsExtension); + + // various others + registry.bind(named("buildName", String.class), toSingleton(buildSpec.getName())); + registry.bind(named("siteName", String.class), toSingleton(buildSpec.getSiteName().get(() -> + new SsgException("the siteName Property in " + buildSpec.getName() + " must be set.") + ))); + registry.bind(named("baseUrl", String.class), toSingleton(buildSpec.getBaseUrl().get(() -> + new SsgException("the baseUrl Property in " + buildSpec.getName() + " must be set.") + ))); + }); + } + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/DefaultPageContextFactory.groovy b/api/src/main/groovy/com/jessebrault/ssg/DefaultPageContextFactory.groovy new file mode 100644 index 0000000..852cc7f --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/DefaultPageContextFactory.groovy @@ -0,0 +1,68 @@ +package com.jessebrault.ssg + +import com.jessebrault.di.ObjectFactory +import com.jessebrault.ssg.view.SkipTemplate +import com.jessebrault.ssg.view.WvcCompiler +import com.jessebrault.ssg.view.WvcPageView +import groowt.view.component.factory.ComponentFactories +import groowt.view.component.web.DefaultWebViewComponentContext +import groowt.view.component.web.WebViewComponent +import groowt.view.component.web.WebViewComponentContext +import groowt.view.component.web.WebViewComponentScope + +class DefaultPageContextFactory implements PageContextFactory { + + protected WebViewComponent makeComponent( + ObjectFactory objectFactory, + Class wvcClass, + Map attr, + Object[] args + ) { + if (!attr.isEmpty() && args.length > 0) { + return objectFactory.createInstance(wvcClass, attr, *args) + } else if (!attr.isEmpty()) { + return objectFactory.createInstance(wvcClass, attr) + } else if (args.length > 0) { + return objectFactory.createInstance(wvcClass, *args) + } else { + return objectFactory.createInstance(wvcClass) + } + } + + @Override + WebViewComponentContext makeContext( + WvcPageView wvcPageView, + ObjectFactory buildObjectFactory, + Set> allWvcClasses + ) { + new DefaultWebViewComponentContext().tap { + configureRootScope(WebViewComponentScope) { + // custom components + allWvcClasses.each { wvcClass -> + //noinspection GroovyAssignabilityCheck + add(wvcClass, ComponentFactories.ofClosureClassType(wvcClass) { Map attr, Object[] args -> + // instantiate component and set context + WebViewComponent component = makeComponent(buildObjectFactory, wvcClass, attr, args) + component.context = wvcPageView.context + + // set the template + if (component.componentTemplate == null && !wvcClass.isAnnotationPresent(SkipTemplate)) { + def compileResult = buildObjectFactory.createInstance(WvcCompiler).compileTemplate( + wvcClass, + wvcClass.simpleName + 'Template.wvc' + ) + if (compileResult.isRight()) { + component.componentTemplate = compileResult.getRight() + } else { + def left = compileResult.getLeft() + throw new RuntimeException(left.message, left.exception) + } + } + return component + }) + } + } + } + } + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/DefaultPageRenderer.java b/api/src/main/groovy/com/jessebrault/ssg/DefaultPageRenderer.java new file mode 100644 index 0000000..1dd5c43 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/DefaultPageRenderer.java @@ -0,0 +1,62 @@ +package com.jessebrault.ssg; + +import com.jessebrault.di.ObjectFactory; +import com.jessebrault.fp.either.Either; +import com.jessebrault.ssg.page.Page; +import com.jessebrault.ssg.util.Diagnostic; +import com.jessebrault.ssg.view.PageView; +import com.jessebrault.ssg.view.WvcPageView; +import groowt.view.component.web.WebViewComponent; +import jakarta.inject.Inject; + +import java.io.StringWriter; +import java.util.Set; + +public class DefaultPageRenderer implements PageRenderer { + + private final PageContextFactory pageContextFactory; + + @Inject + public DefaultPageRenderer(PageContextFactory pageContextFactory) { + this.pageContextFactory = pageContextFactory; + } + + @Override + public Either renderPage( + Page page, + String baseUrl, + ObjectFactory buildObjectFactory, + Set> allWvcClasses + ) { + // create the view + final Either viewResult = page.createView(); + if (viewResult.isLeft()) { + return Either.left(viewResult.getLeft()); + } + final PageView pageView = viewResult.getRight(); + + // prepare for rendering + // set props + pageView.setPageTitle(page.getName()); + pageView.setUrl(baseUrl + page.getPath()); + + // set context if WvcPageView + if (pageView instanceof WvcPageView wvcPageView) { + wvcPageView.setContext(this.pageContextFactory.makeContext(wvcPageView, buildObjectFactory, allWvcClasses)); + } + + // Render page + final var sw = new StringWriter(); + try { + pageView.renderTo(sw); + } catch (Exception exception) { + return Either.left(new Diagnostic( + "There was an exception while rendering " + page.getName() + " as " + pageView.getClass().getName(), + exception + )); + } + + return Either.right(sw.toString()); + } + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/DefaultPageScanner.java b/api/src/main/groovy/com/jessebrault/ssg/DefaultPageScanner.java new file mode 100644 index 0000000..2a63f61 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/DefaultPageScanner.java @@ -0,0 +1,80 @@ +package com.jessebrault.ssg; + +import com.jessebrault.di.ObjectFactory; +import com.jessebrault.ssg.page.DefaultWvcPage; +import com.jessebrault.ssg.page.Page; +import com.jessebrault.ssg.page.PageFactory; +import com.jessebrault.ssg.page.PageSpec; +import com.jessebrault.ssg.view.PageView; +import com.jessebrault.ssg.view.WvcCompiler; +import io.github.classgraph.AnnotationInfo; +import io.github.classgraph.ClassInfoList; +import io.github.classgraph.ScanResult; +import jakarta.inject.Inject; + +import java.util.*; +import java.util.concurrent.*; + +public class DefaultPageScanner implements PageScanner { + + private final ExecutorService executorService; + private final WvcCompiler wvcCompiler; + + @Inject + public DefaultPageScanner(ExecutorService executorService, WvcCompiler wvcCompiler) { + this.executorService = executorService; + this.wvcCompiler = wvcCompiler; + } + + @Override + public Set getAllPages(ScanResult scanResult, ObjectFactory buildObjectFactory) { + final Set results = ConcurrentHashMap.newKeySet(); + + // Start fetching single pages + final ClassInfoList pageViewInfoList = scanResult.getClassesImplementing(PageView.class); + final List> pageViewFutures = pageViewInfoList.stream() + .map(classInfo -> { + return CompletableFuture.runAsync(() -> { + final AnnotationInfo annotationInfo = classInfo.getAnnotationInfo(PageSpec.class); + if (annotationInfo != null) { + final PageSpec pageSpec = (PageSpec) annotationInfo.loadClassAndInstantiate(); + results.add(new DefaultWvcPage(Map.of( + "name", pageSpec.name(), + "path", pageSpec.path(), + "fileExtension", pageSpec.fileExtension(), + "viewType", classInfo.loadClass(), + "templateResource", pageSpec.templateResource().isEmpty() + ? classInfo.getSimpleName() + "Template.wvc" + : pageSpec.templateResource(), + "objectFactory", buildObjectFactory, + "wvcCompiler", this.wvcCompiler + ))); + } + }, this.executorService); + }) + .toList(); + + // Start fetching page factories + final ClassInfoList pageFactoryInfoList = scanResult.getClassesImplementing(PageFactory.class); + final List> pageFactoryFutures = pageFactoryInfoList.stream() + .map(classInfo -> { + return CompletableFuture.runAsync(() -> { + final Class pageFactoryClass = classInfo.loadClass(PageFactory.class); + final PageFactory pageFactory = buildObjectFactory.createInstance(pageFactoryClass); + final Collection pages = pageFactory.create(); + results.addAll(pages); + }, this.executorService); + }) + .toList(); + + // join + final List> allFutures = new ArrayList<>(); + allFutures.addAll(pageViewFutures); + allFutures.addAll(pageFactoryFutures); + + CompletableFuture.allOf(allFutures.toArray(new CompletableFuture[0])).join(); + + return results; + } + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/DefaultStaticSiteGenerator.groovy b/api/src/main/groovy/com/jessebrault/ssg/DefaultStaticSiteGenerator.groovy index dea47a3..f498721 100644 --- a/api/src/main/groovy/com/jessebrault/ssg/DefaultStaticSiteGenerator.groovy +++ b/api/src/main/groovy/com/jessebrault/ssg/DefaultStaticSiteGenerator.groovy @@ -183,8 +183,6 @@ class DefaultStaticSiteGenerator implements StaticSiteGenerator { @Override Collection doBuild( - File projectDir, - String buildName, String buildScriptFqn, Map buildScriptCliArgs ) { diff --git a/api/src/main/groovy/com/jessebrault/ssg/DefaultTextsGetter.java b/api/src/main/groovy/com/jessebrault/ssg/DefaultTextsGetter.java new file mode 100644 index 0000000..3e93769 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/DefaultTextsGetter.java @@ -0,0 +1,25 @@ +package com.jessebrault.ssg; + +import com.jessebrault.ssg.buildscript.BuildSpec; +import com.jessebrault.ssg.text.Text; +import com.jessebrault.ssg.text.TextSupplier; + +import java.util.HashSet; +import java.util.Set; + +public class DefaultTextsGetter implements TextsGetter { + + @Override + public Set getTexts(BuildSpec buildSpec) { + final Set textSuppliers = buildSpec.getTextSuppliers().get(() -> + new SsgException("The textSuppliers Property in " + buildSpec.getName() + + " must contain at least an empty Set.") + ); + final Set texts = new HashSet<>(); + for (final var textSupplier : textSuppliers) { + texts.addAll(textSupplier.get()); + } + return texts; + } + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/DefaultWvcCompilerFactory.java b/api/src/main/groovy/com/jessebrault/ssg/DefaultWvcCompilerFactory.java new file mode 100644 index 0000000..ab90956 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/DefaultWvcCompilerFactory.java @@ -0,0 +1,22 @@ +package com.jessebrault.ssg; + +import com.jessebrault.ssg.view.WvcCompiler; +import groovy.lang.GroovyClassLoader; +import groowt.view.component.compiler.SimpleComponentTemplateClassFactory; +import jakarta.inject.Inject; + +public class DefaultWvcCompilerFactory implements WvcCompilerFactory { + + private final GroovyClassLoader groovyClassLoader; + + @Inject + public DefaultWvcCompilerFactory(GroovyClassLoader groovyClassLoader) { + this.groovyClassLoader = groovyClassLoader; + } + + @Override + public WvcCompiler getWvcCompiler() { + return new WvcCompiler(this.groovyClassLoader, new SimpleComponentTemplateClassFactory(this.groovyClassLoader)); + } + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/FilePageWriter.java b/api/src/main/groovy/com/jessebrault/ssg/FilePageWriter.java new file mode 100644 index 0000000..6ba2604 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/FilePageWriter.java @@ -0,0 +1,43 @@ +package com.jessebrault.ssg; + +import com.jessebrault.ssg.page.Page; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; + +public class FilePageWriter implements PageWriter { + + @Override + public void write(Page page, File outputDir, String renderedPage) { + if (!outputDir.mkdirs()) { + throw new RuntimeException("Could not make directories for outputDir " + outputDir); + }; + + // calculate target path + final List pathParts = Arrays.asList(page.getPath().split("/")); + if (page.getPath().endsWith("/")) { + pathParts.add("index"); + } + + final String head = pathParts.getFirst(); + final List tail = pathParts.size() > 1 ? pathParts.subList(1, pathParts.size()) : List.of(); + + final Path path = Path.of(head, tail.toArray(String[]::new)); + final File outputFile = new File(outputDir, path + page.getFileExtension()); + + // make dirs and write + if (!outputFile.getParentFile().mkdirs()) { + throw new RuntimeException("Could not make parent directories for " + outputFile); + } + try (final FileWriter writer = new FileWriter(outputFile)) { + writer.write(renderedPage); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/JDefaultStaticSiteGenerator.java b/api/src/main/groovy/com/jessebrault/ssg/JDefaultStaticSiteGenerator.java new file mode 100644 index 0000000..94f651c --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/JDefaultStaticSiteGenerator.java @@ -0,0 +1,140 @@ +package com.jessebrault.ssg; + +import com.jessebrault.di.BindingUtil; +import com.jessebrault.di.RegistryObjectFactory; +import com.jessebrault.fp.either.Either; +import com.jessebrault.ssg.buildscript.BuildSpec; +import com.jessebrault.ssg.buildscript.BuildSpecFactory; +import com.jessebrault.ssg.di.PagesExtension; +import com.jessebrault.ssg.di.SelfPageExtension; +import com.jessebrault.ssg.page.Page; +import com.jessebrault.ssg.util.Diagnostic; +import groovy.lang.GroovyClassLoader; +import groowt.view.component.web.WebViewComponent; +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ScanResult; +import jakarta.inject.Inject; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; + +public class JDefaultStaticSiteGenerator implements StaticSiteGenerator { + + private final BuildSpecFactory buildSpecFactory; + private final ObjectFactoryConfigurator objectFactoryConfigurator; + private final GroovyClassLoader groovyClassLoader; + private final ExecutorService executorService; + private final PageScanner pageScanner; + private final ComponentClassScanner componentClassScanner; + private final PageRenderer pageRenderer; + private final PageWriter pageWriter; + + @Inject + public JDefaultStaticSiteGenerator( + BuildSpecFactory buildSpecFactory, + ObjectFactoryConfigurator objectFactoryConfigurator, + GroovyClassLoader groovyClassLoader, + ExecutorService executorService, + PageScanner pageScanner, + ComponentClassScanner componentClassScanner, + PageRenderer pageRenderer, + PageWriter pageWriter + ) { + this.buildSpecFactory = buildSpecFactory; + this.objectFactoryConfigurator = objectFactoryConfigurator; + this.groovyClassLoader = groovyClassLoader; + this.executorService = executorService; + this.pageScanner = pageScanner; + this.componentClassScanner = componentClassScanner; + this.pageRenderer = pageRenderer; + this.pageWriter = pageWriter; + } + + + @Override + public Collection doBuild(String buildScriptFqn, Map buildScriptCliArgs) { + // Get build spec + final BuildSpec buildSpec = this.buildSpecFactory.getBuildSpec(buildScriptFqn, buildScriptCliArgs); + + // Prepare object factory for rendering pages and components + final RegistryObjectFactory buildObjectFactory = buildSpec.getObjectFactory().get(() -> + new SsgException("objectFactory Provider in " + buildSpec.getName() + " must be set.") + ); + this.objectFactoryConfigurator.configure(buildObjectFactory, buildSpec); + + // ClassGraph scan of base packages + final Set basePackages = buildSpec.getBasePackages().get(() -> + new SsgException("basePackages Provider in " + buildSpec.getName() + " must be at least an empty Set.") + ); + final ClassGraph classGraph = new ClassGraph() + .enableAnnotationInfo() + .addClassLoader(this.groovyClassLoader); + for (final String basePackage : basePackages) { + classGraph.acceptPackages(basePackage); + } + + // Get all pages and components from scan + final Set pages = ConcurrentHashMap.newKeySet(); + final Set> componentClasses = ConcurrentHashMap.newKeySet(); + + try (final ScanResult scanResult = classGraph.scan()) { + // fork + final CompletableFuture pagesFuture = CompletableFuture.runAsync( + () -> pages.addAll(this.pageScanner.getAllPages(scanResult, buildObjectFactory)), + this.executorService + ); + final CompletableFuture componentsFuture = CompletableFuture.runAsync( + () -> componentClasses.addAll(this.componentClassScanner.getWebViewComponentClasses(scanResult)), + this.executorService + ); + + // join + CompletableFuture.allOf(pagesFuture, componentsFuture).join(); + } + + // final ObjectFactory configuration to add all pages/components found AND self page extension + buildObjectFactory.configureRegistry(registry -> { + final var pagesExtension = new PagesExtension(); + pagesExtension.getAllPages().addAll(pages); + registry.addExtension(pagesExtension); + + registry.bind(BindingUtil.named("allWvc", Set.class), BindingUtil.toSingleton(componentClasses)); + + registry.addExtension(new SelfPageExtension()); + }); + + // render each page + final Set diagnostics = ConcurrentHashMap.newKeySet(); + + final List> renderAndWriteFutures = pages.stream() + .map(page -> CompletableFuture.runAsync(() -> { + final Either renderResult = this.pageRenderer.renderPage( + page, + buildSpec.getBaseUrl().get(() -> + new SsgException("baseUrl Provider in " + buildSpec.getName() + " must be set.") + ), + buildObjectFactory, + componentClasses + ); + if (renderResult.isLeft()) { + diagnostics.add(renderResult.getLeft()); + return; + } + this.pageWriter.write( + page, + buildSpec.getOutputDir().get(() -> + new SsgException("outputDir Provider in " + buildSpec.getName() + " must be set.") + ), + renderResult.getRight() + ); + }, this.executorService)) + .toList(); + + CompletableFuture.allOf(renderAndWriteFutures.toArray(new CompletableFuture[0])).join(); + + return diagnostics; + } + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/ObjectFactoryConfigurator.java b/api/src/main/groovy/com/jessebrault/ssg/ObjectFactoryConfigurator.java new file mode 100644 index 0000000..9b7f4b4 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/ObjectFactoryConfigurator.java @@ -0,0 +1,8 @@ +package com.jessebrault.ssg; + +import com.jessebrault.di.RegistryObjectFactory; +import com.jessebrault.ssg.buildscript.BuildSpec; + +public interface ObjectFactoryConfigurator { + void configure(RegistryObjectFactory registryObjectFactory, BuildSpec buildSpec); +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/PageContextFactory.java b/api/src/main/groovy/com/jessebrault/ssg/PageContextFactory.java new file mode 100644 index 0000000..e7d68eb --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/PageContextFactory.java @@ -0,0 +1,16 @@ +package com.jessebrault.ssg; + +import com.jessebrault.di.ObjectFactory; +import com.jessebrault.ssg.view.WvcPageView; +import groowt.view.component.web.WebViewComponent; +import groowt.view.component.web.WebViewComponentContext; + +import java.util.Set; + +public interface PageContextFactory { + WebViewComponentContext makeContext( + WvcPageView wvcPageView, + ObjectFactory buildObjectFactory, + Set> allWvcClasses + ); +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/PageRenderer.java b/api/src/main/groovy/com/jessebrault/ssg/PageRenderer.java new file mode 100644 index 0000000..3bd3436 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/PageRenderer.java @@ -0,0 +1,18 @@ +package com.jessebrault.ssg; + +import com.jessebrault.di.ObjectFactory; +import com.jessebrault.fp.either.Either; +import com.jessebrault.ssg.page.Page; +import com.jessebrault.ssg.util.Diagnostic; +import groowt.view.component.web.WebViewComponent; + +import java.util.Set; + +public interface PageRenderer { + Either renderPage( + Page page, + String baseUrl, + ObjectFactory buildObjectFactory, + Set> allWvcClasses + ); +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/PageScanner.java b/api/src/main/groovy/com/jessebrault/ssg/PageScanner.java new file mode 100644 index 0000000..4b4f5ed --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/PageScanner.java @@ -0,0 +1,11 @@ +package com.jessebrault.ssg; + +import com.jessebrault.di.ObjectFactory; +import com.jessebrault.ssg.page.Page; +import io.github.classgraph.ScanResult; + +import java.util.Set; + +public interface PageScanner { + Set getAllPages(ScanResult scanResult, ObjectFactory buildObjectFactory); +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/PageWriter.java b/api/src/main/groovy/com/jessebrault/ssg/PageWriter.java new file mode 100644 index 0000000..f1f7eca --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/PageWriter.java @@ -0,0 +1,9 @@ +package com.jessebrault.ssg; + +import com.jessebrault.ssg.page.Page; + +import java.io.File; + +public interface PageWriter { + void write(Page page, File outputDir, String renderedPage); +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/StaticSiteGenerator.java b/api/src/main/groovy/com/jessebrault/ssg/StaticSiteGenerator.java index fa6729e..b6314d6 100644 --- a/api/src/main/groovy/com/jessebrault/ssg/StaticSiteGenerator.java +++ b/api/src/main/groovy/com/jessebrault/ssg/StaticSiteGenerator.java @@ -1,12 +1,13 @@ -package com.jessebrault.ssg +package com.jessebrault.ssg; -import com.jessebrault.ssg.util.Diagnostic +import com.jessebrault.ssg.util.Diagnostic; -interface StaticSiteGenerator { +import java.util.Collection; +import java.util.Map; + +public interface StaticSiteGenerator { Collection doBuild( - File projectDir, - String buildName, String buildScriptFqn, Map buildScriptCliArgs - ) + ); } diff --git a/api/src/main/groovy/com/jessebrault/ssg/TextsGetter.java b/api/src/main/groovy/com/jessebrault/ssg/TextsGetter.java new file mode 100644 index 0000000..40b4743 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/TextsGetter.java @@ -0,0 +1,10 @@ +package com.jessebrault.ssg; + +import com.jessebrault.ssg.buildscript.BuildSpec; +import com.jessebrault.ssg.text.Text; + +import java.util.Set; + +public interface TextsGetter { + Set getTexts(BuildSpec buildSpec); +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/WvcCompilerFactory.java b/api/src/main/groovy/com/jessebrault/ssg/WvcCompilerFactory.java new file mode 100644 index 0000000..20fb39d --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/WvcCompilerFactory.java @@ -0,0 +1,7 @@ +package com.jessebrault.ssg; + +import com.jessebrault.ssg.view.WvcCompiler; + +public interface WvcCompilerFactory { + WvcCompiler getWvcCompiler(); +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/WvcContextFactory.java b/api/src/main/groovy/com/jessebrault/ssg/WvcContextFactory.java new file mode 100644 index 0000000..9e91dd1 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/WvcContextFactory.java @@ -0,0 +1,8 @@ +package com.jessebrault.ssg; + +import com.jessebrault.ssg.view.WvcPageView; +import groowt.view.component.web.WebViewComponentContext; + +public interface WvcContextFactory { + WebViewComponentContext getContext(WvcPageView currentPage); +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildScriptBase.groovy b/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildScriptBase.groovy index 9cff114..e1f7006 100644 --- a/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildScriptBase.groovy +++ b/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildScriptBase.groovy @@ -1,5 +1,6 @@ package com.jessebrault.ssg.buildscript +import com.jessebrault.di.ObjectFactory import com.jessebrault.ssg.buildscript.delegates.BuildDelegate import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.Nullable @@ -25,6 +26,8 @@ abstract class BuildScriptBase extends Script { private Closure buildClosure = { } private File projectRoot private String buildName + private ObjectFactory objectFactory + private Map cliArgs /* --- Instance DSL helpers --- */ @@ -73,4 +76,21 @@ abstract class BuildScriptBase extends Script { this.buildClosure } + ObjectFactory getObjectFactory() { + return objectFactory + } + + @ApiStatus.Internal + void setObjectFactory(ObjectFactory objectFactory) { + this.objectFactory = objectFactory + } + + Map getCliArgs() { + return cliArgs + } + + void setCliArgs(Map cliArgs) { + this.cliArgs = cliArgs + } + } diff --git a/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildScriptFactory.java b/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildScriptFactory.java new file mode 100644 index 0000000..1313bd5 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildScriptFactory.java @@ -0,0 +1,7 @@ +package com.jessebrault.ssg.buildscript; + +import java.util.Map; + +public interface BuildScriptFactory { + BuildScriptBase getAndRunBuildScript(String scriptFqn, Map scriptCliArgs); +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildScriptToBuildSpecConverter.groovy b/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildScriptToBuildSpecConverter.groovy index 663d4b2..3469fc1 100644 --- a/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildScriptToBuildSpecConverter.groovy +++ b/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildScriptToBuildSpecConverter.groovy @@ -5,13 +5,12 @@ import groovy.transform.NullCheck import groovy.transform.TupleConstructor import java.util.function.Function -import java.util.function.Supplier @NullCheck @TupleConstructor(includeFields = true) class BuildScriptToBuildSpecConverter { - private final BuildScriptGetter buildScriptGetter + private final BuildScriptFactory buildScriptFactory private final Function buildDelegateFactory protected BuildSpec getFromDelegate(String name, BuildDelegate delegate) { diff --git a/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildSpec.groovy b/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildSpec.groovy index da24d9d..d39d8b6 100644 --- a/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildSpec.groovy +++ b/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildSpec.groovy @@ -1,11 +1,11 @@ package com.jessebrault.ssg.buildscript -import com.jessebrault.ssg.model.Model -import com.jessebrault.ssg.text.TextConverter -import groovy.transform.EqualsAndHashCode -import groovy.transform.NullCheck import com.jessebrault.di.RegistryObjectFactory import com.jessebrault.fp.provider.Provider +import com.jessebrault.ssg.model.Model +import com.jessebrault.ssg.text.TextSupplier +import groovy.transform.EqualsAndHashCode +import groovy.transform.NullCheck import static com.jessebrault.ssg.util.ObjectUtil.requireProvider import static com.jessebrault.ssg.util.ObjectUtil.requireString @@ -21,9 +21,8 @@ final class BuildSpec { final Provider outputDir final Provider> globals final Provider> models - final Provider> textsDirs - final Provider> textConverters - final Provider objectFactoryBuilder + final Provider> textSuppliers + final Provider objectFactory @SuppressWarnings('GroovyAssignabilityCheck') BuildSpec(Map args) { @@ -34,15 +33,14 @@ final class BuildSpec { this.outputDir = requireProvider(args.outputDir) this.globals = requireProvider(args.globals) this.models = requireProvider(args.models) - this.textsDirs = requireProvider(args.textsDirs) - this.textConverters = requireProvider(args.textConverters) - this.objectFactoryBuilder = requireProvider(args.objectFactoryBuilder) + this.textSuppliers = requireProvider(args.textSuppliers) + this.objectFactory = requireProvider(args.objectFactory) } @Override String toString() { "Build(name: ${this.name}, basePackages: $basePackages, siteName: $siteName, " + - "baseUrl: $baseUrl, outputDir: $outputDir, textsDirs: $textsDirs)" + "baseUrl: $baseUrl, outputDir: $outputDir, textSuppliers: $textSuppliers)" } } diff --git a/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildSpecFactory.java b/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildSpecFactory.java new file mode 100644 index 0000000..097888f --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildSpecFactory.java @@ -0,0 +1,7 @@ +package com.jessebrault.ssg.buildscript; + +import java.util.Map; + +public interface BuildSpecFactory { + BuildSpec getBuildSpec(String scriptFqn, Map scriptCliArgs); +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/buildscript/DefaultBuildScriptFactory.java b/api/src/main/groovy/com/jessebrault/ssg/buildscript/DefaultBuildScriptFactory.java new file mode 100644 index 0000000..88dba81 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/buildscript/DefaultBuildScriptFactory.java @@ -0,0 +1,78 @@ +package com.jessebrault.ssg.buildscript; + +import com.jessebrault.di.ObjectFactory; +import groovy.lang.GroovyClassLoader; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.codehaus.groovy.control.CompilerConfiguration; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.util.List; +import java.util.Map; + +public class DefaultBuildScriptFactory implements BuildScriptFactory { + + private final GroovyClassLoader groovyClassLoader; + private final List scriptBaseUrls; + private final File projectDir; + private final ObjectFactory objectFactory; + + @Inject + public DefaultBuildScriptFactory( + GroovyClassLoader groovyClassLoader, + @Named("scriptBaseUrls") List scriptBaseUrls, + @Named("projectDir") File projectDir, + ObjectFactory objectFactory + ) { + this.groovyClassLoader = groovyClassLoader; + this.scriptBaseUrls = scriptBaseUrls; + this.projectDir = projectDir; + this.objectFactory = objectFactory; + } + + protected GroovyClassLoader getScriptClassLoader() { + // set up gcl with our base script class + final var compilerConfiguration = new CompilerConfiguration(); + compilerConfiguration.setScriptBaseClass(BuildScriptBase.class.getName()); + final var scriptGroovyClassLoader = new GroovyClassLoader( + this.groovyClassLoader, + compilerConfiguration + ); + + // add urls where to find scripts + for (final var url : this.scriptBaseUrls) { + scriptGroovyClassLoader.addURL(url); + } + + return scriptGroovyClassLoader; + } + + @Override + public BuildScriptBase getAndRunBuildScript(String scriptFqn, Map scriptCliArgs) { + try (final GroovyClassLoader scriptClassLoader = this.getScriptClassLoader()) { + // Get script instance + @SuppressWarnings("unchecked") + final Class scriptClass = (Class) + scriptClassLoader.loadClass(scriptFqn, true, true); + final BuildScriptBase script = scriptClass.getConstructor().newInstance(); + + // configure props + script.setProjectRoot(this.projectDir); + script.setBuildName(scriptFqn); + script.setObjectFactory(this.objectFactory); + script.setCliArgs(scriptCliArgs); + + // run + script.run(); + + return script; + } catch (IOException | ClassNotFoundException | NoSuchMethodException | InstantiationException | + IllegalAccessException | InvocationTargetException exception) { + throw new RuntimeException(exception); + } + } + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/buildscript/DefaultBuildSpecFactory.java b/api/src/main/groovy/com/jessebrault/ssg/buildscript/DefaultBuildSpecFactory.java new file mode 100644 index 0000000..65b49c9 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/buildscript/DefaultBuildSpecFactory.java @@ -0,0 +1,64 @@ +package com.jessebrault.ssg.buildscript; + +import com.jessebrault.ssg.buildscript.delegates.BuildDelegate; +import com.jessebrault.ssg.buildscript.delegates.BuildDelegateConfigurator; +import com.jessebrault.ssg.buildscript.delegates.BuildDelegateConverter; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.util.Deque; +import java.util.LinkedList; +import java.util.Map; + +public class DefaultBuildSpecFactory implements BuildSpecFactory { + + private final BuildScriptFactory buildScriptFactory; + private final BuildDelegateConfigurator buildDelegateConfigurator; + private final BuildDelegateConverter buildDelegateConverter; + private final File projectDir; + + @Inject + public DefaultBuildSpecFactory( + BuildScriptFactory buildScriptFactory, + BuildDelegateConfigurator buildDelegateConfigurator, + BuildDelegateConverter buildDelegateConverter, + @Named("projectDir") File projectDir + ) { + this.buildScriptFactory = buildScriptFactory; + this.buildDelegateConfigurator = buildDelegateConfigurator; + this.buildDelegateConverter = buildDelegateConverter; + this.projectDir = projectDir; + } + + protected BuildSpec doConvert(String scriptFqn, Map scriptCliArgs, BuildScriptBase script) { + // 1. Make hierarchy as a stack + final Deque buildHierarchy = new LinkedList<>(); + buildHierarchy.push(script); + @Nullable String extending = script.getExtending(); + while (extending != null) { + final BuildScriptBase from = this.buildScriptFactory.getAndRunBuildScript(extending, scriptCliArgs); + buildHierarchy.push(from); + extending = from.getExtending(); + } + + // Go through the stack from top to bottom, using the same delegate + final BuildDelegate buildDelegate = new BuildDelegate(this.projectDir); + this.buildDelegateConfigurator.configure(buildDelegate, scriptFqn); + while (!buildHierarchy.isEmpty()) { + final BuildScriptBase from = buildHierarchy.pop(); + from.getBuildClosure().setDelegate(buildDelegate); + from.getBuildClosure().run(); + } + + return this.buildDelegateConverter.convert(scriptFqn, buildDelegate); + } + + @Override + public BuildSpec getBuildSpec(String scriptFqn, Map scriptCliArgs) { + final BuildScriptBase script = this.buildScriptFactory.getAndRunBuildScript(scriptFqn, scriptCliArgs); + return this.doConvert(scriptFqn, scriptCliArgs, script); + } + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/buildscript/delegates/BuildDelegate.groovy b/api/src/main/groovy/com/jessebrault/ssg/buildscript/delegates/BuildDelegate.groovy index 7cd76f6..2907600 100644 --- a/api/src/main/groovy/com/jessebrault/ssg/buildscript/delegates/BuildDelegate.groovy +++ b/api/src/main/groovy/com/jessebrault/ssg/buildscript/delegates/BuildDelegate.groovy @@ -1,35 +1,18 @@ package com.jessebrault.ssg.buildscript.delegates -import com.jessebrault.ssg.model.Model -import com.jessebrault.ssg.model.Models -import com.jessebrault.ssg.text.MarkdownTextConverter -import com.jessebrault.ssg.text.TextConverter -import com.jessebrault.ssg.util.PathUtil -import com.jessebrault.di.DefaultRegistryObjectFactory import com.jessebrault.di.RegistryObjectFactory import com.jessebrault.fp.property.DefaultProperty import com.jessebrault.fp.property.Property -import com.jessebrault.fp.provider.DefaultProvider import com.jessebrault.fp.provider.NamedProvider import com.jessebrault.fp.provider.Provider +import com.jessebrault.ssg.model.Model +import com.jessebrault.ssg.model.Models +import com.jessebrault.ssg.text.TextSupplier -import java.nio.file.Path import java.util.function.Supplier final class BuildDelegate { - static BuildDelegate withDefaults(String buildName, File projectDir) { - new BuildDelegate(projectDir).tap { - basePackages.convention = [] as Set - outputDir.convention = PathUtil.resolve(projectDir, Path.of('dist', buildName.split(/\\./))) - globals.convention = [:] - models.convention = [] as Set - textsDirs.convention = [new File(projectDir, 'texts')] as Set - textConverters.convention = [new MarkdownTextConverter()] as Set - objectFactoryBuilder.convention = DefaultRegistryObjectFactory.Builder.withDefaults() - } - } - final File projectDir final Property> basePackages = DefaultProperty.>empty(Set) @@ -38,12 +21,10 @@ final class BuildDelegate { final Property outputDir = DefaultProperty.empty(File) final Property> globals = DefaultProperty.>empty(Map) final Property> models = DefaultProperty.>empty(Set) - final Property> textsDirs = DefaultProperty.>empty(Set) - final Property> textConverters = DefaultProperty.>empty(Set) - final Property objectFactoryBuilder = - DefaultProperty.empty(RegistryObjectFactory.Builder) + final Property> textSuppliers = DefaultProperty.>empty(Set) + final Property objectFactory = DefaultProperty.empty(RegistryObjectFactory) - private BuildDelegate(File projectDir) { + BuildDelegate(File projectDir) { this.projectDir = projectDir } @@ -109,24 +90,12 @@ final class BuildDelegate { this.models.configure { it.add(Models.ofNamedProvider(namedProvider)) } } - void textsDir(File textsDir) { - this.textsDirs.configure { it.add(textsDir) } + void textSupplier(TextSupplier toAdd) { + this.textSuppliers.configure { it.add(toAdd) } } - void textsDirs(File... textsDirs) { - textsDirs.each { this.textsDir(it) } - } - - void textConverter(TextConverter textConverter) { - this.textConverters.configure { it.add(textConverter) } - } - - void textConverters(TextConverter... textConverters) { - textConverters.each { this.textConverter(it) } - } - - void objectFactoryBuilder(RegistryObjectFactory.Builder builder) { - this.objectFactoryBuilder.set(builder) + void textSuppliers(TextSupplier... toAdd) { + this.textSuppliers.configure { it.addAll(toAdd) } } } diff --git a/api/src/main/groovy/com/jessebrault/ssg/buildscript/delegates/BuildDelegateConfigurator.java b/api/src/main/groovy/com/jessebrault/ssg/buildscript/delegates/BuildDelegateConfigurator.java new file mode 100644 index 0000000..08597e5 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/buildscript/delegates/BuildDelegateConfigurator.java @@ -0,0 +1,5 @@ +package com.jessebrault.ssg.buildscript.delegates; + +public interface BuildDelegateConfigurator { + void configure(BuildDelegate buildDelegate, String buildName); +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/buildscript/delegates/BuildDelegateConverter.java b/api/src/main/groovy/com/jessebrault/ssg/buildscript/delegates/BuildDelegateConverter.java new file mode 100644 index 0000000..b6c9dd6 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/buildscript/delegates/BuildDelegateConverter.java @@ -0,0 +1,7 @@ +package com.jessebrault.ssg.buildscript.delegates; + +import com.jessebrault.ssg.buildscript.BuildSpec; + +public interface BuildDelegateConverter { + BuildSpec convert(String buildName, BuildDelegate buildDelegate); +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/buildscript/delegates/DefaultBuildDelegateConfigurator.groovy b/api/src/main/groovy/com/jessebrault/ssg/buildscript/delegates/DefaultBuildDelegateConfigurator.groovy new file mode 100644 index 0000000..a25168d --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/buildscript/delegates/DefaultBuildDelegateConfigurator.groovy @@ -0,0 +1,39 @@ +package com.jessebrault.ssg.buildscript.delegates + +import com.jessebrault.di.DefaultRegistryObjectFactory +import com.jessebrault.ssg.model.Model +import com.jessebrault.ssg.text.TextSupplier +import com.jessebrault.ssg.text.TextsDirMarkdownTextSupplier +import com.jessebrault.ssg.util.PathUtil +import jakarta.inject.Inject +import jakarta.inject.Named + +import java.nio.file.Path + +class DefaultBuildDelegateConfigurator implements BuildDelegateConfigurator { + + private final File projectDir + private final TextsDirMarkdownTextSupplier textsDirMarkdownTextSupplier + + @Inject + DefaultBuildDelegateConfigurator( + @Named('projectDir') File projectDir, + TextsDirMarkdownTextSupplier textsDirMarkdownTextSupplier + ) { + this.projectDir = projectDir + this.textsDirMarkdownTextSupplier = textsDirMarkdownTextSupplier + } + + @Override + void configure(BuildDelegate buildDelegate, String buildName) { + buildDelegate.tap { + basePackages.convention = [] as Set + outputDir.convention = PathUtil.resolve(this.projectDir, Path.of('dist', buildName.split(/\\./))) + globals.convention = [:] + models.convention = [] as Set + textSuppliers.convention = [this.textsDirMarkdownTextSupplier] as Set + objectFactory.convention = DefaultRegistryObjectFactory.Builder.withDefaults().build() + } + } + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/buildscript/delegates/DefaultBuildDelegateConverter.groovy b/api/src/main/groovy/com/jessebrault/ssg/buildscript/delegates/DefaultBuildDelegateConverter.groovy new file mode 100644 index 0000000..dee641e --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/buildscript/delegates/DefaultBuildDelegateConverter.groovy @@ -0,0 +1,22 @@ +package com.jessebrault.ssg.buildscript.delegates + +import com.jessebrault.ssg.buildscript.BuildSpec + +class DefaultBuildDelegateConverter implements BuildDelegateConverter { + + @Override + BuildSpec convert(String buildName, BuildDelegate delegate) { + return new BuildSpec( + name: buildName, + basePackages: delegate.basePackages, + siteName: delegate.siteName, + baseUrl: delegate.baseUrl, + outputDir: delegate.outputDir, + globals: delegate.globals, + models: delegate.models, + textSuppliers: delegate.textSuppliers, + objectFactory: delegate.objectFactory + ) + } + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/text/TextSupplier.java b/api/src/main/groovy/com/jessebrault/ssg/text/TextSupplier.java new file mode 100644 index 0000000..8e302c9 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/text/TextSupplier.java @@ -0,0 +1,8 @@ +package com.jessebrault.ssg.text; + +import java.util.Collection; + +@FunctionalInterface +public interface TextSupplier { + Collection get(); +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/text/TextsDirMarkdownTextSupplier.java b/api/src/main/groovy/com/jessebrault/ssg/text/TextsDirMarkdownTextSupplier.java new file mode 100644 index 0000000..6c16b41 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/text/TextsDirMarkdownTextSupplier.java @@ -0,0 +1,58 @@ +package com.jessebrault.ssg.text; + +import jakarta.inject.Inject; +import jakarta.inject.Named; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.stream.Stream; + +public class TextsDirMarkdownTextSupplier implements TextSupplier { + + private final File projectDir; + private final ExecutorService executorService; + private final MarkdownTextConverter markdownTextConverter; + + @Inject + public TextsDirMarkdownTextSupplier( + @Named("projectDir") File projectDir, + ExecutorService executorService, + MarkdownTextConverter markdownTextConverter + ) { + this.projectDir = projectDir; + this.executorService = executorService; + this.markdownTextConverter = markdownTextConverter; + } + + @Override + public Collection get() { + final Path textsDir = Paths.get(projectDir.getAbsolutePath(), "texts"); + final Collection results = ConcurrentHashMap.newKeySet(); + if (Files.exists(textsDir)) { + try (final Stream walkStream = Files.walk(textsDir)){ + final List> textFutures = walkStream + .map(path -> { + return CompletableFuture.runAsync(() -> { + if (path.endsWith(".md")) { + results.add(this.markdownTextConverter.convert(textsDir.toFile(), path.toFile())); + } + }, this.executorService); + }) + .toList(); + CompletableFuture.allOf(textFutures.toArray(new CompletableFuture[0])).join(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return results; + } + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/util/Diagnostic.groovy b/api/src/main/groovy/com/jessebrault/ssg/util/Diagnostic.groovy index 45c10c2..1c8c980 100644 --- a/api/src/main/groovy/com/jessebrault/ssg/util/Diagnostic.groovy +++ b/api/src/main/groovy/com/jessebrault/ssg/util/Diagnostic.groovy @@ -4,13 +4,17 @@ import groovy.transform.EqualsAndHashCode import groovy.transform.TupleConstructor import org.jetbrains.annotations.Nullable -@TupleConstructor @EqualsAndHashCode final class Diagnostic { final String message final @Nullable Exception exception + Diagnostic(String message, Exception exception) { + this.message = message + this.exception = exception + } + @Override String toString() { if (this.exception != null) { diff --git a/api/src/main/groovy/com/jessebrault/ssg/view/WvcCompiler.groovy b/api/src/main/groovy/com/jessebrault/ssg/view/WvcCompiler.groovy index dc6a85e..7a74037 100644 --- a/api/src/main/groovy/com/jessebrault/ssg/view/WvcCompiler.groovy +++ b/api/src/main/groovy/com/jessebrault/ssg/view/WvcCompiler.groovy @@ -1,15 +1,13 @@ package com.jessebrault.ssg.view -import com.jessebrault.ssg.util.Diagnostic -import groovy.transform.TupleConstructor import com.jessebrault.fp.either.Either +import com.jessebrault.ssg.util.Diagnostic import groowt.view.component.ComponentTemplate import groowt.view.component.ViewComponent import groowt.view.component.compiler.ComponentTemplateClassFactory import groowt.view.component.compiler.source.ComponentTemplateSource import groowt.view.component.web.compiler.DefaultWebViewComponentTemplateCompileUnit -@TupleConstructor class WvcCompiler { private static class SsgWvcTemplateCompileUnit extends DefaultWebViewComponentTemplateCompileUnit { @@ -30,6 +28,11 @@ class WvcCompiler { final GroovyClassLoader groovyClassLoader final ComponentTemplateClassFactory templateClassFactory + WvcCompiler(GroovyClassLoader groovyClassLoader, ComponentTemplateClassFactory templateClassFactory) { + this.groovyClassLoader = groovyClassLoader + this.templateClassFactory = templateClassFactory + } + Either compileTemplate( Class componentClass, String resourceName @@ -37,7 +40,8 @@ class WvcCompiler { def templateUrl = componentClass.getResource(resourceName) if (templateUrl == null) { return Either.left(new Diagnostic( - "Could not find templateResource: $resourceName" + "Could not find templateResource: $resourceName", + null )) } def source = ComponentTemplateSource.of(templateUrl) diff --git a/buildSrc/src/main/groovy/ssg-common.gradle b/buildSrc/src/main/groovy/ssg-common.gradle index bf88143..e0cb1d1 100644 --- a/buildSrc/src/main/groovy/ssg-common.gradle +++ b/buildSrc/src/main/groovy/ssg-common.gradle @@ -4,7 +4,7 @@ plugins { } group 'com.jessebrault.ssg' -version '0.6.3' +version '0.7.0-SNAPSHOT' repositories { mavenCentral() diff --git a/cli/src/main/groovy/com/jessebrault/ssg/AbstractBuildCommand.groovy b/cli/src/main/groovy/com/jessebrault/ssg/AbstractBuildCommand.groovy index 2cbf581..4b79e11 100644 --- a/cli/src/main/groovy/com/jessebrault/ssg/AbstractBuildCommand.groovy +++ b/cli/src/main/groovy/com/jessebrault/ssg/AbstractBuildCommand.groovy @@ -1,8 +1,20 @@ package com.jessebrault.ssg +import com.jessebrault.di.DefaultRegistryObjectFactory +import com.jessebrault.di.ObjectFactory +import com.jessebrault.di.RegistryObjectFactory +import com.jessebrault.ssg.buildscript.BuildScriptFactory +import com.jessebrault.ssg.buildscript.BuildSpecFactory +import com.jessebrault.ssg.buildscript.DefaultBuildScriptFactory +import com.jessebrault.ssg.buildscript.DefaultBuildSpecFactory +import com.jessebrault.ssg.buildscript.delegates.BuildDelegateConfigurator +import com.jessebrault.ssg.buildscript.delegates.BuildDelegateConverter +import com.jessebrault.ssg.buildscript.delegates.DefaultBuildDelegateConfigurator +import com.jessebrault.ssg.buildscript.delegates.DefaultBuildDelegateConverter import com.jessebrault.ssg.gradle.SsgBuildModel import com.jessebrault.ssg.util.Diagnostic import com.jessebrault.ssg.util.URLUtil +import com.jessebrault.ssg.view.WvcCompiler import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger import org.gradle.tooling.GradleConnector @@ -10,6 +22,10 @@ import picocli.CommandLine import java.nio.file.Files import java.nio.file.Path +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +import static com.jessebrault.di.BindingUtil.* abstract class AbstractBuildCommand extends AbstractSubCommand { @@ -74,8 +90,47 @@ abstract class AbstractBuildCommand extends AbstractSubCommand { ) boolean profile + @CommandLine.Option( + names = ['-t', '--threads'], + description = 'The number of threads to use.', + defaultValue = '8' + ) + Integer threads + + protected RegistryObjectFactory objectFactory = null protected StaticSiteGenerator staticSiteGenerator = null + protected RegistryObjectFactory getApiObjectFactory( + GroovyClassLoader groovyClassLoader, + ExecutorService executorService, + PageWriter pageWriter, + List scriptBaseUrls, + File projectDir + ) { + RegistryObjectFactory objectFactory = DefaultRegistryObjectFactory.Builder.withDefaults().build() + objectFactory.tap { + configureRegistry { + bind(BuildSpecFactory, toClass(DefaultBuildSpecFactory)) + bind(ObjectFactoryConfigurator, toClass(DefaultObjectFactoryConfigurator)) + bind(GroovyClassLoader, toSingleton(groovyClassLoader)) + bind(ExecutorService, toSingleton(executorService)) + bind(PageScanner, toClass(DefaultPageScanner)) + bind(ComponentClassScanner, toClass(DefaultComponentClassScanner)) + bind(PageRenderer, toClass(DefaultPageRenderer)) + bind(PageWriter, toSingleton(pageWriter)) + bind(BuildScriptFactory, toClass(DefaultBuildScriptFactory)) + bind(BuildDelegateConfigurator, toClass(DefaultBuildDelegateConfigurator)) + bind(BuildDelegateConverter, toClass(DefaultBuildDelegateConverter)) + bind(named('scriptBaseUrls', List), toSingleton(scriptBaseUrls)) + bind(named('projectDir', File), toSingleton(projectDir)) + bind(TextsGetter, toClass(DefaultTextsGetter)) + bind(WvcCompiler, toSelf()) + bind(PageContextFactory, toClass(DefaultPageContextFactory)) + bind(ObjectFactory, toSingleton(objectFactory)) + } + } + } + protected final Integer doSingleBuild(String buildName) { logger.traceEntry('buildName: {}', buildName) @@ -126,23 +181,21 @@ abstract class AbstractBuildCommand extends AbstractSubCommand { def buildScriptDirUrls = this.buildScriptDirs.collect { def withProjectDir = new File(this.commonCliOptions.projectDir, it.toString()) withProjectDir.toURI().toURL() - } as URL[] + } - this.staticSiteGenerator = new DefaultStaticSiteGenerator( + this.objectFactory = this.getApiObjectFactory( groovyClassLoader, + Executors.newFixedThreadPool(this.threads), + !this.dryRun ? new FilePageWriter() : new ConsolePageWriter(), buildScriptDirUrls, - this.dryRun + new File('.') ) + this.staticSiteGenerator = objectFactory.createInstance(JDefaultStaticSiteGenerator) } def buildStartTime = System.currentTimeMillis() - final Collection diagnostics = this.staticSiteGenerator.doBuild( - this.commonCliOptions.projectDir, - buildName, - buildName, - this.scriptArgs ?: [:] - ) + final Collection diagnostics = this.staticSiteGenerator.doBuild(buildName, this.scriptArgs ?: [:]) def buildElapsedTime = System.currentTimeMillis() - buildStartTime if (this.profile) { diff --git a/cli/src/main/groovy/com/jessebrault/ssg/StaticSiteGeneratorCli.groovy b/cli/src/main/groovy/com/jessebrault/ssg/StaticSiteGeneratorCli.groovy index 3b9362f..253c539 100644 --- a/cli/src/main/groovy/com/jessebrault/ssg/StaticSiteGeneratorCli.groovy +++ b/cli/src/main/groovy/com/jessebrault/ssg/StaticSiteGeneratorCli.groovy @@ -5,7 +5,7 @@ import picocli.CommandLine @CommandLine.Command( name = 'ssg', mixinStandardHelpOptions = true, - version = '0.6.3', + version = '0.7.0-SNAPSHOT', description = 'A static site generator which can interface with Gradle for high extensibility.', subcommands = [SsgInit, SsgBuild, SsgWatch] ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f0a4c51..e955fa6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -classgraph = '4.8.179' +classgraph = '4.8.184' commonmark = '0.24.0' di = '0.1.0' fp = '0.1.0' diff --git a/ssg-gradle-plugin/src/main/java/com/jessebrault/ssg/gradle/SsgGradlePlugin.java b/ssg-gradle-plugin/src/main/java/com/jessebrault/ssg/gradle/SsgGradlePlugin.java index 538332e..295c3dd 100644 --- a/ssg-gradle-plugin/src/main/java/com/jessebrault/ssg/gradle/SsgGradlePlugin.java +++ b/ssg-gradle-plugin/src/main/java/com/jessebrault/ssg/gradle/SsgGradlePlugin.java @@ -162,8 +162,8 @@ public class SsgGradlePlugin implements Plugin { Configuration ssgApiConfiguration, Configuration ssgCliConfiguration ) { - final Dependency ssgApi = project.getDependencies().create("com.jessebrault.ssg:api:0.6.3"); - final Dependency ssgCli = project.getDependencies().create("com.jessebrault.ssg:cli:0.6.3"); + final Dependency ssgApi = project.getDependencies().create("com.jessebrault.ssg:api:0.7.0-SNAPSHOT"); + final Dependency ssgCli = project.getDependencies().create("com.jessebrault.ssg:cli:0.7.0-SNAPSHOT"); ssgApiConfiguration.getDependencies().add(ssgApi); ssgCliConfiguration.getDependencies().add(ssgCli); }