Lots of work refactoring and introduction of StaticSiteGenerator and implementation.

This commit is contained in:
JesseBrault0709 2023-05-01 20:57:35 +02:00
parent 7708ac66e0
commit 5a70f9c91c
16 changed files with 437 additions and 51 deletions

View File

@ -8,6 +8,7 @@ Here will be kept all of the various todos for this project, organized by releas
- [ ] Plan out plugin system such that we can create custom providers of texts, data, etc.
- [ ] Provide a way to override `ssgBuilds` variables from the cli.
- [ ] Add `Watchable` interface/trait back; an abstraction over FS watching and other sources (such as a database, etc.).
- [ ] Explore `apply(Plugin)` in buildScripts.
### Fix
@ -18,6 +19,7 @@ Here will be kept all of the various todos for this project, organized by releas
- [x] Remove `lib` module.
- [ ] Add a way for CLI to choose a build to do, or multiple builds, defaulting to 'default' if it exists.
- [ ] Write lots of tests for buildscript dsl, etc.
- [ ] Explore `base` in buildScript dsl.
### Fix
- [ ] Update CHANGELOG to reflect the gsp-dsl changes.

View File

@ -0,0 +1,104 @@
package com.jessebrault.ssg
import com.jessebrault.ssg.buildscript.Build
import com.jessebrault.ssg.buildscript.BuildScriptConfiguratorFactory
import com.jessebrault.ssg.buildscript.BuildScriptRunner
import com.jessebrault.ssg.util.Diagnostic
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.Marker
import org.slf4j.MarkerFactory
import java.util.function.Consumer
final class BuildScriptBasedStaticSiteGenerator implements StaticSiteGenerator {
private static final Logger logger = LoggerFactory.getLogger(BuildScriptBasedStaticSiteGenerator)
private static final Marker enter = MarkerFactory.getMarker('enter')
private static final Marker exit = MarkerFactory.getMarker('exit')
private final BuildScriptRunner buildScriptRunner
private final BuildScriptConfiguratorFactory configuratorFactory
private final File buildScript
private final Collection<File> buildSrcDirs
private final Map<String, Object> scriptArgs
private final Collection<Build> builds = []
private boolean ranBuildScript = false
BuildScriptBasedStaticSiteGenerator(
BuildScriptRunner buildScriptRunner,
BuildScriptConfiguratorFactory configuratorFactory,
File buildScript = null,
Collection<File> buildSrcDirs = [],
Map<String, Object> scriptArgs = [:]
) {
this.buildScriptRunner = buildScriptRunner
this.configuratorFactory = configuratorFactory
this.buildScript = buildScript
this.buildSrcDirs = buildSrcDirs
this.scriptArgs = scriptArgs
}
private void runBuildScript() {
logger.trace(enter, '')
if (this.buildScript == null) {
logger.info('no specified build script; using defaults')
def result = this.buildScriptRunner.runBuildScript {
this.configuratorFactory.get().accept(it)
}
this.builds.addAll(result)
} else if (this.buildScript.exists() && this.buildScript.isFile()) {
logger.info('running buildScript: {}', this.buildScript)
def result = this.buildScriptRunner.runBuildScript(
this.buildScript.name,
this.buildScript.parentFile.toURI().toURL(),
this.buildSrcDirs.collect { it.toURI().toURL() },
[args: this.scriptArgs]
) {
this.configuratorFactory.get().accept(it)
}
this.builds.addAll(result)
} else {
throw new IllegalArgumentException("given buildScript ${ this.buildScript } either does not exist or is not a file")
}
this.ranBuildScript = true
logger.trace(exit, '')
}
@Override
boolean doBuild(String buildName, Consumer<Collection<Diagnostic>> diagnosticsConsumer = { }) {
logger.trace(enter, 'buildName: {}, diagnosticsConsumer: {}', buildName, diagnosticsConsumer)
if (!this.ranBuildScript) {
this.runBuildScript()
}
def build = this.builds.find { it.name == buildName }
if (!build) {
throw new IllegalArgumentException("there is no registered build with name: ${ buildName }")
}
def buildTasksConverter = new SimpleBuildTasksConverter()
def successful = true
def tasksResult = buildTasksConverter.convert(build)
if (tasksResult.hasDiagnostics()) {
successful = false
diagnosticsConsumer.accept(tasksResult.diagnostics)
} else {
def tasks = tasksResult.get()
def taskDiagnostics = tasks.collectMany { it.execute(tasks) }
if (!taskDiagnostics.isEmpty()) {
successful = false
diagnosticsConsumer.accept(taskDiagnostics)
}
}
logger.trace(exit, 'successful: {}', successful)
successful
}
}

View File

@ -0,0 +1,9 @@
package com.jessebrault.ssg
import com.jessebrault.ssg.util.Diagnostic
import java.util.function.Consumer
interface StaticSiteGenerator {
boolean doBuild(String buildName, Consumer<Collection<Diagnostic>> diagnosticsConsumer)
}

View File

@ -1,5 +1,8 @@
package com.jessebrault.ssg.buildscript
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SimpleType
import java.util.function.Consumer
interface BuildScriptRunner {
@ -12,6 +15,12 @@ interface BuildScriptRunner {
Consumer<BuildScriptBase> configureBuildScript
)
Collection<Build> runBuildScript(
@DelegatesTo(value = BuildScriptBase, strategy = Closure.DELEGATE_FIRST)
@ClosureParams(value = SimpleType, options = 'com.jessebrault.ssg.buildscript.BuildScriptBase')
Closure<?> scriptBody
)
default Collection<Build> runBuildScript(
String scriptName,
URL scriptBaseDirUrl,

View File

@ -1,6 +1,8 @@
package com.jessebrault.ssg.buildscript
import groovy.transform.NullCheck
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SimpleType
import org.codehaus.groovy.control.CompilerConfiguration
import java.util.function.Consumer
@ -29,4 +31,24 @@ final class SimpleBuildScriptRunner implements BuildScriptRunner {
buildScript.getBuilds()
}
@Override
Collection<Build> runBuildScript(
@DelegatesTo(value = BuildScriptBase, strategy = Closure.DELEGATE_FIRST)
@ClosureParams(value = SimpleType, options = 'com.jessebrault.ssg.buildscript.BuildScriptBase')
Closure<?> scriptBody
) {
def base = new BuildScriptBase() {
@Override
Object run() {
scriptBody.delegate = this
scriptBody.resolveStrategy = Closure.DELEGATE_FIRST
scriptBody.call(this)
}
}
base.run()
base.getBuilds()
}
}

View File

@ -30,7 +30,7 @@ final class FileBasedCollectionProvider<T> extends AbstractCollectionProvider<T>
@Override
Collection<T> provide() {
if (!this.baseDirectory.isDirectory()) {
logger.error('{} does not exist or is not a directory; returning empty collection', this.baseDirectory)
logger.warn('{} does not exist or is not a directory; returning empty collection', this.baseDirectory)
[]
} else {
final Collection<T> ts = []

View File

@ -0,0 +1,60 @@
package com.jessebrault.ssg
import com.jessebrault.ssg.buildscript.BuildScriptBase
import com.jessebrault.ssg.buildscript.BuildScriptConfiguratorFactory
import com.jessebrault.ssg.buildscript.SimpleBuildScriptRunner
import com.jessebrault.ssg.util.FileUtil
import org.junit.jupiter.api.Test
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.function.Consumer
import static org.junit.jupiter.api.Assertions.assertTrue
// TODO: everything is working, now to expand this and refactor out common test code.
final class BuildScriptBasedStaticSiteGeneratorTests {
private static final Logger logger = LoggerFactory.getLogger(BuildScriptBasedStaticSiteGeneratorTests)
@Test
void buildWithOneTextAndTemplate() {
def sourceDir = File.createTempDir()
def buildScript = new File(sourceDir, 'build.groovy')
FileUtil.copyResourceToFile('oneTextAndTemplate.groovy', buildScript)
new FileTreeBuilder(sourceDir).tap {
dir('texts') {
file('hello.md', '---\ntemplate: hello.gsp\n---\nHello, World!')
}
dir('templates') {
file('hello.gsp', '<%= text.render() %>')
}
}
def ssg = new BuildScriptBasedStaticSiteGenerator(
new SimpleBuildScriptRunner(),
new BuildScriptConfiguratorFactory() {
@Override
Consumer<BuildScriptBase> get() {
return { }
}
},
buildScript,
[],
[sourceDir: sourceDir]
)
assertTrue(ssg.doBuild('test') {
it.each { logger.error(it.toString()) }
})
def expectedBase = File.createTempDir()
new File(expectedBase, 'hello.html').write('<p>Hello, World!</p>\n')
FileUtil.assertFileStructureAndContents(expectedBase, new File(sourceDir, 'build'))
}
}

View File

@ -17,6 +17,8 @@ final class SimpleBuildScriptRunnerTests {
/**
* Must be non-static, otherwise Groovy gets confused inside the Closures.
*
* TODO: use the FileUtil.copyResourceToWriter method
*/
@SuppressWarnings('GrMethodMayBeStatic')
private void copyLocalResourceToWriter(String name, Writer target) {
@ -95,4 +97,14 @@ final class SimpleBuildScriptRunnerTests {
verify(stringConsumer).accept('test')
}
@Test
void customScript() {
def runner = new SimpleBuildScriptRunner()
def result = runner.runBuildScript {
build('test') { }
}
assertEquals(1, result.size())
assertEquals('test', result[0].name)
}
}

View File

@ -0,0 +1,48 @@
import com.jessebrault.ssg.buildscript.Build
import com.jessebrault.ssg.buildscript.BuildScriptBase
import com.jessebrault.ssg.buildscript.OutputDir
import com.jessebrault.ssg.html.TextToHtmlSpec
import com.jessebrault.ssg.html.TextToHtmlTaskFactory
import com.jessebrault.ssg.provider.CollectionProviders
import com.jessebrault.ssg.template.TemplateTypes
import com.jessebrault.ssg.template.TemplatesProviders
import com.jessebrault.ssg.text.TextTypes
import com.jessebrault.ssg.text.TextsProviders
import com.jessebrault.ssg.util.ExtensionUtil
import com.jessebrault.ssg.util.Result
import groovy.transform.BaseScript
@BaseScript
BuildScriptBase base
build('test') {
outputDirFunction = { Build b -> new OutputDir(new File(args.sourceDir, 'build')) }
types {
textTypes << TextTypes.MARKDOWN
templateTypes << TemplateTypes.GSP
}
providers { types ->
texts TextsProviders.from(new File(args.sourceDir, 'texts'), types.textTypes)
templates TemplatesProviders.from(new File(args.sourceDir, 'templates'), types.templateTypes)
}
taskFactories { sources ->
register('textToHtml', TextToHtmlTaskFactory::new) {
it.specProvider += CollectionProviders.fromSupplier {
def templates = sources.templatesProvider.provide()
return sources.textsProvider.provide().collect {
def frontMatterResult = it.type.frontMatterGetter.get(it)
if (frontMatterResult.hasDiagnostics()) {
return Result.ofDiagnostics(frontMatterResult.diagnostics)
}
def desiredTemplate = frontMatterResult.get().get('template')
def template = templates.find { it.path == desiredTemplate }
if (template == null) {
throw new IllegalArgumentException('template is null')
}
return Result.of(new TextToHtmlSpec(it, template, ExtensionUtil.stripExtension(it.path) + '.html'))
}
}
}
}
}

View File

@ -0,0 +1,65 @@
package com.jessebrault.ssg.util
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.nio.file.Path
import static org.junit.jupiter.api.Assertions.assertEquals
final class FileUtil {
private static final Logger logger = LoggerFactory.getLogger(FileUtil)
private static Map<String, Object> fileToMap(File file) {
[
type: file.isDirectory() ? 'directory' : (file.isFile() ? 'file' : null),
text: file.file ? file.text : null
]
}
static void assertFileStructureAndContents(
File expectedBaseDir,
File actualBaseDir
) {
final Map<Path, File> expectedPathsAndFiles = [:]
def expectedBasePath = expectedBaseDir.toPath()
expectedBaseDir.eachFileRecurse {
def relativePath = expectedBasePath.relativize(it.toPath())
expectedPathsAndFiles[relativePath] = it
}
logger.debug('expectedPaths: {}', expectedPathsAndFiles.keySet())
expectedPathsAndFiles.forEach { relativePath, expectedFile ->
def actualFile = new File(actualBaseDir, relativePath.toString())
logger.debug(
'relativePath: {}, expectedFile: {}, actualFile: {}',
relativePath,
fileToMap(expectedFile),
fileToMap(actualFile)
)
assertEquals(expectedFile.directory, actualFile.directory)
assertEquals(expectedFile.file, actualFile.file)
if (expectedFile.file) {
assertEquals(expectedFile.text, actualFile.text)
}
}
}
static void copyResourceToWriter(String name, Writer target) {
FileUtil.getClassLoader().getResourceAsStream(name).withReader {
it.transferTo(target)
}
}
static void copyResourceToFile(String name, File target) {
FileUtil.getClassLoader().getResourceAsStream(name).withReader { Reader reader ->
target.withWriter { Writer writer ->
reader.transferTo(writer)
}
}
}
private FileUtil() {}
}

View File

@ -0,0 +1,42 @@
package com.jessebrault.ssg.util
import org.junit.jupiter.api.Test
import static com.jessebrault.ssg.util.FileUtil.assertFileStructureAndContents
import static com.jessebrault.ssg.util.FileUtil.copyResourceToFile
import static com.jessebrault.ssg.util.FileUtil.copyResourceToWriter
import static org.junit.jupiter.api.Assertions.assertEquals
final class FileUtilTests {
@Test
void sameStructureAndContentsTest() {
def b0 = File.createTempDir()
def b1 = File.createTempDir()
[b0, b1].each {
new FileTreeBuilder(it).tap {
file('testFile', 'test content')
dir('testDir') {
file('testNestedFile', 'test content')
}
}
}
assertFileStructureAndContents(b0, b1)
}
@Test
void copyResourceToWriterTest() {
def writer = new StringWriter()
copyResourceToWriter('testResource.txt', writer)
assertEquals('Hello, World!', writer.toString())
}
@Test
void copyResourceToTargetFileTest() {
def tempDir = File.createTempDir()
def target = new File(tempDir, 'testResource.txt')
copyResourceToFile('testResource.txt', target)
assertEquals('Hello, World!', target.text)
}
}

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Configuration name="ssg" status="WARN">
<Appenders>
<Console name="standard" target="SYSTEM_OUT">
<PatternLayout>
<MarkerPatternSelector defaultPattern="%highlight{%-5level} %logger{1}: %msg%n%ex">
<PatternMatch key="FLOW" pattern="%highlight{%-5level} %logger{1}: %markerSimpleName %msg%n%ex" />
</MarkerPatternSelector>
</PatternLayout>
</Console>
</Appenders>
<Loggers>
<Root level="trace">
<AppenderRef ref="standard" />
</Root>
</Loggers>
</Configuration>

View File

@ -0,0 +1 @@
Hello, World!

View File

@ -52,6 +52,12 @@ dependencies {
// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core
testRuntimeOnly 'org.apache.logging.log4j:log4j-core:2.19.0'
// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl
testFixturesRuntimeOnly 'org.apache.logging.log4j:log4j-slf4j-impl:2.19.0'
// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core
testFixturesRuntimeOnly 'org.apache.logging.log4j:log4j-core:2.19.0'
}
test {

View File

@ -1,8 +1,7 @@
package com.jessebrault.ssg
import com.jessebrault.ssg.buildscript.Build
import com.jessebrault.ssg.buildscript.SimpleBuildScriptRunner
import com.jessebrault.ssg.buildscript.DefaultBuildScriptConfiguratorFactory
import com.jessebrault.ssg.buildscript.SimpleBuildScriptRunner
import com.jessebrault.ssg.util.Diagnostic
import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger
@ -12,7 +11,26 @@ abstract class AbstractBuildCommand extends AbstractSubCommand {
private static final Logger logger = LogManager.getLogger(AbstractBuildCommand)
protected Collection<Build> availableBuilds = []
@CommandLine.Option(
names = ['-s', '--script', '--buildScript'],
description = 'The build script file to execute.'
)
protected File buildScript = null
@CommandLine.Option(
names = '--scriptArgs',
description = 'Named argument(s) to pass directly to the build script.',
split = ','
)
protected Map<String, String> scriptArgs = [:]
@CommandLine.Option(
names = '--buildSrcDirs',
description = 'Path(s) to director(ies) containing Groovy classes and scripts which should be visible to the main build script.',
split = ',',
paramLabel = 'buildSrcDir'
)
protected Collection<File> buildSrcDirs = [new File('buildSrc')]
@CommandLine.Option(
names = ['-b', '--build'],
@ -21,57 +39,28 @@ abstract class AbstractBuildCommand extends AbstractSubCommand {
)
protected Collection<String> requestedBuilds = ['default']
@CommandLine.Option(
names = ['-s', '--script', '--buildScript'],
description = 'The build script file to execute.',
paramLabel = 'buildScript'
)
void setBuildFile(File buildScriptFile) {
if (buildScriptFile == null) {
buildScriptFile = new File('ssgBuilds.groovy')
}
if (buildScriptFile.exists()) {
logger.info('found buildScriptFile: {}', buildScriptFile)
def configuratorFactory = new DefaultBuildScriptConfiguratorFactory()
this.availableBuilds = new SimpleBuildScriptRunner().runBuildScript(
buildScriptFile.name,
buildScriptFile.parentFile.toURI().toURL(),
[new File('buildSrc').toURI().toURL()],
[:]
) {
configuratorFactory.get().accept(it)
}
logger.debug('after running buildScriptFile {}, builds: {}', buildScriptFile, this.availableBuilds)
} else {
throw new IllegalArgumentException(
"buildScriptFile file ${ buildScriptFile } does not exist or could not be found."
)
}
}
protected StaticSiteGenerator staticSiteGenerator = null
protected final Integer doBuild(String requestedBuild) {
protected final Integer doSingleBuild(String requestedBuild) {
logger.traceEntry('requestedBuild: {}', requestedBuild)
def buildTasksConverter = new SimpleBuildTasksConverter()
def build = this.availableBuilds.find { it.name == requestedBuild }
def hadDiagnostics = false
def tasksResult = buildTasksConverter.convert(build)
if (tasksResult.hasDiagnostics()) {
hadDiagnostics = true
tasksResult.diagnostics.each { logger.error(it.message) }
} else {
def tasks = tasksResult.get()
Collection<Diagnostic> taskDiagnostics = []
tasks.each { it.execute(tasks) }
if (!taskDiagnostics.isEmpty()) {
hadDiagnostics = true
taskDiagnostics.each { logger.error(it.message) }
}
if (this.staticSiteGenerator == null) {
this.staticSiteGenerator = new BuildScriptBasedStaticSiteGenerator(
new SimpleBuildScriptRunner(),
new DefaultBuildScriptConfiguratorFactory(),
this.buildScript,
this.buildSrcDirs,
this.scriptArgs
)
}
logger.traceExit(hadDiagnostics ? 1 : 0)
final Collection<Diagnostic> diagnostics = []
if (!this.staticSiteGenerator.doBuild(requestedBuild, diagnostics.&addAll)) {
diagnostics.each { logger.warn(it) }
logger.traceExit(1)
} else {
logger.traceExit(0)
}
}
}

View File

@ -18,7 +18,7 @@ final class SsgBuild extends AbstractBuildCommand {
logger.traceEntry()
def result = 0
this.requestedBuilds.each {
def buildResult = this.doBuild(it)
def buildResult = this.doSingleBuild(it)
if (buildResult == 1) {
result = 1
}