From 703ee41f13eff7b40527a56a628a8db5f672ccef Mon Sep 17 00:00:00 2001 From: David Herman Date: Wed, 9 Feb 2022 00:16:33 -0800 Subject: [PATCH] Add support for static site layouts This should make it possible for users to export their site in a format easy to deploy on a static hosting site, e.g. Netlify This disables some of the features that make Kobweb powerful (e.g. dynamic routes and server API routes), but for many use cases, e.g. portfolios or blog sites, this is fine. Bug #22 --- .../com/varabyte/kobweb/server/Application.kt | 6 +- .../varabyte/kobweb/server/plugins/Routing.kt | 68 ++++++++++- .../varabyte/kobweb/cli/common/KobwebUtils.kt | 11 +- .../com/varabyte/kobweb/cli/export/Export.kt | 7 +- .../kotlin/com/varabyte/kobweb/cli/run/Run.kt | 11 +- cli/kobweb/src/main/kotlin/main.kt | 14 ++- .../varabyte/kobweb/server/api/SiteLayout.kt | 42 +++++++ .../application/KobwebApplicationPlugin.kt | 10 +- .../application/tasks/KobwebExportTask.kt | 115 ++++++++++++------ .../application/tasks/KobwebStartTask.kt | 3 + 10 files changed, 227 insertions(+), 60 deletions(-) create mode 100644 common/kobweb-common/src/main/kotlin/com/varabyte/kobweb/server/api/SiteLayout.kt diff --git a/backend/server/src/main/kotlin/com/varabyte/kobweb/server/Application.kt b/backend/server/src/main/kotlin/com/varabyte/kobweb/server/Application.kt index f13b4b1ff..2d4ce1d6d 100644 --- a/backend/server/src/main/kotlin/com/varabyte/kobweb/server/Application.kt +++ b/backend/server/src/main/kotlin/com/varabyte/kobweb/server/Application.kt @@ -3,6 +3,7 @@ package com.varabyte.kobweb.server import com.varabyte.kobweb.common.error.KobwebException import com.varabyte.kobweb.project.KobwebFolder import com.varabyte.kobweb.project.conf.KobwebConfFile +import com.varabyte.kobweb.server.api.SiteLayout import com.varabyte.kobweb.server.api.ServerEnvironment import com.varabyte.kobweb.server.api.ServerRequest import com.varabyte.kobweb.server.api.ServerRequestsFile @@ -11,7 +12,6 @@ import com.varabyte.kobweb.server.io.ServerStateFile import com.varabyte.kobweb.server.plugins.configureHTTP import com.varabyte.kobweb.server.plugins.configureRouting import com.varabyte.kobweb.server.plugins.configureSerialization -import io.ktor.application.* import io.ktor.server.engine.* import io.ktor.server.netty.* import kotlinx.coroutines.delay @@ -60,10 +60,10 @@ fun main() = runBlocking { throw KobwebException("Production server can't start as port $port is already occupied. If you need a different port number, consider modifying ${confFile.path}") } } - val globals = ServerGlobals() + val siteLayout = SiteLayout.get() val engine = embeddedServer(Netty, port) { - configureRouting(env, conf, globals) + configureRouting(env, siteLayout, conf, globals) configureSerialization() configureHTTP(conf) } diff --git a/backend/server/src/main/kotlin/com/varabyte/kobweb/server/plugins/Routing.kt b/backend/server/src/main/kotlin/com/varabyte/kobweb/server/plugins/Routing.kt index 3e721e6b3..34f388929 100644 --- a/backend/server/src/main/kotlin/com/varabyte/kobweb/server/plugins/Routing.kt +++ b/backend/server/src/main/kotlin/com/varabyte/kobweb/server/plugins/Routing.kt @@ -6,6 +6,7 @@ import com.varabyte.kobweb.api.http.Request import com.varabyte.kobweb.api.log.Logger import com.varabyte.kobweb.project.conf.KobwebConf import com.varabyte.kobweb.server.ServerGlobals +import com.varabyte.kobweb.server.api.SiteLayout import com.varabyte.kobweb.server.api.ServerEnvironment import com.varabyte.kobweb.server.io.ApiJarFile import io.ktor.application.* @@ -21,7 +22,6 @@ import kotlinx.coroutines.withContext import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import java.io.File -import java.io.IOException import java.nio.file.Path import kotlin.io.path.Path import kotlin.io.path.name @@ -29,7 +29,7 @@ import kotlin.io.path.name /** Somewhat uniqueish parameter key name so it's unlikely to clash with anything a user would choose by chance. */ private const val KOBWEB_PARAMS = "kobweb-params" -fun Application.configureRouting(env: ServerEnvironment, conf: KobwebConf, globals: ServerGlobals) { +fun Application.configureRouting(env: ServerEnvironment, siteLayout: SiteLayout, conf: KobwebConf, globals: ServerGlobals) { val logger = object : Logger { override fun trace(message: String) = log.trace(message) override fun debug(message: String) = log.debug(message) @@ -38,10 +38,26 @@ fun Application.configureRouting(env: ServerEnvironment, conf: KobwebConf, globa override fun error(message: String) = log.error(message) } - when (env) { - ServerEnvironment.DEV -> configureDevRouting(conf, globals, logger) - ServerEnvironment.PROD -> configureProdRouting(conf, logger) + if (siteLayout == SiteLayout.STATIC && env != ServerEnvironment.PROD) { + log.warn(""" + Static site layout is configured for a development server. + + This isn't expected, as development servers expect to read their values from the user's project. Static + layouts are really only designed to be used in production. The server will still run in static mode as + requested, but live-reloading, server APIs, etc. will not work with this configuration. + """.trimIndent()) } + + when (siteLayout) { + SiteLayout.KOBWEB -> { + when (env) { + ServerEnvironment.DEV -> configureDevRouting(conf, globals, logger) + ServerEnvironment.PROD -> configureProdRouting(conf, logger) + } + } + SiteLayout.STATIC -> configureStaticRouting(conf) + } + } private suspend fun PipelineContext.handleApiCall( @@ -267,4 +283,46 @@ private fun Application.configureProdRouting(conf: KobwebConf, logger: Logger) { configureCatchAllRouting(script, fallbackIndex) } +} + +/** + * Run a Kobweb server as a dumb, static server. + * + * This is kind of a waste of a Kobweb server, since it has all the smarts removed, but at the same time, it's supported + * so a user can test-run the static site experience which will ultimately be provided by some external provider. + */ +private fun Application.configureStaticRouting(conf: KobwebConf) { + val siteRoot = Path(conf.server.files.prod.siteRoot) + routing { + siteRoot.toFile().let { siteRootFile -> + siteRootFile.walkBottomUp().filter { it.isFile }.forEach { file -> + val relativeFile = file.relativeTo(siteRootFile) + val name = relativeFile.name.removeSuffix(".html") + val parent = relativeFile.parent?.let { "$it/" } ?: "" + if (name != "index") { + get("/$parent$name") { + call.respondFile(file) + } + } else { + get("/$parent") { + call.respondFile(file) + } + } + } + + // Anything not found is an error + val errorFile = siteRootFile.resolve("404.html") + if (errorFile.exists()) { + // Catch URLs of the form a/b/c/ + get("{...}/") { + call.respondFile(errorFile) + } + + // Catch URLs of the form a/b/c/slug + get("{...}") { + call.respondFile(errorFile) + } + } + } + } } \ No newline at end of file diff --git a/cli/kobweb/src/main/kotlin/com/varabyte/kobweb/cli/common/KobwebUtils.kt b/cli/kobweb/src/main/kotlin/com/varabyte/kobweb/cli/common/KobwebUtils.kt index 72d7825ec..3391f139e 100644 --- a/cli/kobweb/src/main/kotlin/com/varabyte/kobweb/cli/common/KobwebUtils.kt +++ b/cli/kobweb/src/main/kotlin/com/varabyte/kobweb/cli/common/KobwebUtils.kt @@ -2,6 +2,7 @@ package com.varabyte.kobweb.cli.common import com.varabyte.kobweb.common.error.KobwebException import com.varabyte.kobweb.project.KobwebProject +import com.varabyte.kobweb.server.api.SiteLayout import com.varabyte.kobweb.server.api.ServerEnvironment import com.varabyte.kotter.runtime.Session @@ -34,8 +35,8 @@ class KobwebGradle(private val env: ServerEnvironment) { return Runtime.getRuntime().gradlew(*finalArgs.toTypedArray()) } - fun startServer(enableLiveReloading: Boolean = (env == ServerEnvironment.DEV)): Process { - val args = mutableListOf("-PkobwebEnv=$env", "kobwebStart") + fun startServer(enableLiveReloading: Boolean, siteLayout: SiteLayout): Process { + val args = mutableListOf("-PkobwebEnv=$env", "-PkobwebRunLayout=$siteLayout", "kobwebStart") if (enableLiveReloading) { args.add("-t") } @@ -46,7 +47,9 @@ class KobwebGradle(private val env: ServerEnvironment) { return gradlew("kobwebStop") } - fun export(): Process { - return gradlew("-PkobwebReuseServer=false", "-PkobwebBuildTarget=RELEASE", "kobwebExport") + fun export(siteLayout: SiteLayout): Process { + // Even if we are exporting a non-Kobweb layout, we still want to start up a dev server using a Kobweb layout so + // it looks for the source files in the right place. + return gradlew("-PkobwebReuseServer=false", "-PkobwebEnv=DEV", "-PkobwebRunLayout=KOBWEB", "-PkobwebBuildTarget=RELEASE", "-PkobwebExportLayout=$siteLayout", "kobwebExport") } } \ No newline at end of file diff --git a/cli/kobweb/src/main/kotlin/com/varabyte/kobweb/cli/export/Export.kt b/cli/kobweb/src/main/kotlin/com/varabyte/kobweb/cli/export/Export.kt index 12eccb612..5b1cb40ca 100644 --- a/cli/kobweb/src/main/kotlin/com/varabyte/kobweb/cli/export/Export.kt +++ b/cli/kobweb/src/main/kotlin/com/varabyte/kobweb/cli/export/Export.kt @@ -7,6 +7,7 @@ import com.varabyte.kobweb.cli.common.findKobwebProject import com.varabyte.kobweb.cli.common.assertKobwebProject import com.varabyte.kobweb.cli.common.handleConsoleOutput import com.varabyte.kobweb.cli.common.newline +import com.varabyte.kobweb.server.api.SiteLayout import com.varabyte.kobweb.server.api.ServerEnvironment import com.varabyte.kotter.foundation.anim.textAnimOf import com.varabyte.kotter.foundation.input.Keys @@ -28,7 +29,7 @@ private enum class ExportState { } @Suppress("BlockingMethodInNonBlockingContext") -fun handleExport(isInteractive: Boolean) { +fun handleExport(siteLayout: SiteLayout, isInteractive: Boolean) { val kobwebGradle = KobwebGradle(ServerEnvironment.PROD) // exporting is a production-only action if (isInteractive) session { @@ -57,7 +58,7 @@ fun handleExport(isInteractive: Boolean) { } }.run { val exportProcess = try { - kobwebGradle.export() + kobwebGradle.export(siteLayout) } catch (ex: Exception) { exception = ex @@ -100,7 +101,7 @@ fun handleExport(isInteractive: Boolean) { } else { assert(!isInteractive) assertKobwebProject() - kobwebGradle.export().also { it.consumeProcessOutput(); it.waitFor() } + kobwebGradle.export(siteLayout).also { it.consumeProcessOutput(); it.waitFor() } kobwebGradle.stopServer().also { it.consumeProcessOutput(); it.waitFor() } } } \ No newline at end of file diff --git a/cli/kobweb/src/main/kotlin/com/varabyte/kobweb/cli/run/Run.kt b/cli/kobweb/src/main/kotlin/com/varabyte/kobweb/cli/run/Run.kt index 3fb2bccd7..44e6b5731 100644 --- a/cli/kobweb/src/main/kotlin/com/varabyte/kobweb/cli/run/Run.kt +++ b/cli/kobweb/src/main/kotlin/com/varabyte/kobweb/cli/run/Run.kt @@ -7,6 +7,7 @@ import com.varabyte.kobweb.cli.common.consumeProcessOutput import com.varabyte.kobweb.cli.common.findKobwebProject import com.varabyte.kobweb.cli.common.handleConsoleOutput import com.varabyte.kobweb.cli.common.newline +import com.varabyte.kobweb.server.api.SiteLayout import com.varabyte.kobweb.server.api.ServerEnvironment import com.varabyte.kobweb.server.api.ServerRequest import com.varabyte.kobweb.server.api.ServerRequestsFile @@ -42,7 +43,11 @@ private enum class RunState { INTERRUPTED, } -fun handleRun(env: ServerEnvironment, isInteractive: Boolean) { +fun handleRun( + env: ServerEnvironment, + siteLayout: SiteLayout, + isInteractive: Boolean, +) { val kobwebGradle = KobwebGradle(env) if (isInteractive) session { val kobwebFolder = findKobwebProject()?.kobwebFolder ?: return@session @@ -107,7 +112,7 @@ fun handleRun(env: ServerEnvironment, isInteractive: Boolean) { } }.runUntilSignal { val startServerProcess = try { - kobwebGradle.startServer() + kobwebGradle.startServer(enableLiveReloading = (env == ServerEnvironment.DEV), siteLayout) } catch (ex: Exception) { exception = ex @@ -198,6 +203,6 @@ fun handleRun(env: ServerEnvironment, isInteractive: Boolean) { assertKobwebProject() // If we're non-interactive, it means we just want to start the Kobweb server and exit without waiting for // for any additional changes. (This is essentially used when run in a web server environment) - kobwebGradle.startServer(enableLiveReloading = false).also { it.consumeProcessOutput(); it.waitFor() } + kobwebGradle.startServer(enableLiveReloading = false, siteLayout).also { it.consumeProcessOutput(); it.waitFor() } } } \ No newline at end of file diff --git a/cli/kobweb/src/main/kotlin/main.kt b/cli/kobweb/src/main/kotlin/main.kt index 859294394..2fb3f00a4 100644 --- a/cli/kobweb/src/main/kotlin/main.kt +++ b/cli/kobweb/src/main/kotlin/main.kt @@ -7,6 +7,7 @@ import com.varabyte.kobweb.cli.list.handleList import com.varabyte.kobweb.cli.run.handleRun import com.varabyte.kobweb.cli.stop.handleStop import com.varabyte.kobweb.cli.version.handleVersion +import com.varabyte.kobweb.server.api.SiteLayout import com.varabyte.kobweb.server.api.ServerEnvironment import kotlinx.cli.ArgParser import kotlinx.cli.ArgType @@ -29,6 +30,13 @@ private fun ArgParser.mode() = option( description = "If interactive, runs in an ANSI-enabled terminal expecting user input. If dumb, command only outputs, using simple console logging", ).default(Mode.INTERACTIVE) +private fun ArgParser.layout() = option( + ArgType.Choice(), + fullName = "layout", + shortName = "l", + description = "Specify the organizational layout of the site files.", +).default(SiteLayout.KOBWEB) + private const val VERSION_HELP = "Print the version of this binary" @ExperimentalCli @@ -66,18 +74,20 @@ fun main(args: Array) { class Export : Subcommand("export", "Generate a static version of a Kobweb app / site") { val mode by mode() + val layout by layout() override fun execute() { - handleExport(mode == Mode.INTERACTIVE) + handleExport(layout, mode == Mode.INTERACTIVE) } } class Run : Subcommand("run", "Run a Kobweb server") { val env by option(ArgType.Choice(), "env").default(ServerEnvironment.DEV) val mode by mode() + val layout by layout() override fun execute() { - handleRun(env, mode == Mode.INTERACTIVE) + handleRun(env, layout, mode == Mode.INTERACTIVE) } } diff --git a/common/kobweb-common/src/main/kotlin/com/varabyte/kobweb/server/api/SiteLayout.kt b/common/kobweb-common/src/main/kotlin/com/varabyte/kobweb/server/api/SiteLayout.kt new file mode 100644 index 000000000..df5a11853 --- /dev/null +++ b/common/kobweb-common/src/main/kotlin/com/varabyte/kobweb/server/api/SiteLayout.kt @@ -0,0 +1,42 @@ +package com.varabyte.kobweb.server.api + +import com.varabyte.kobweb.common.error.KobwebException +import com.varabyte.kobweb.project.conf.KobwebConf + +private val PROPERTY_SITE_LAYOUT = "kobweb.site.layout" + +enum class SiteLayout { + /** + * Files live in multiple roots as specified in the [KobwebConf] file. + * + * Furthermore, the files searched for are different in developement and production versions. + * + * With this format, your pages, resources, scripts, and server jar are served from all potentially different + * locations, with a fallback `index.html` that can be used for unknown URLs. + * + * This format is guaranteed to support all Kobweb features, although it will require you hosting your own server + * which might be more expensive than a static layout. + */ + KOBWEB, + + /** + * Files live in single root folder, from which they are served directly without any complex routing logic. + * + * This is ideal for serving simple static blog sites. External hosting providers should be able to handle this + * layout. + * + * Kobweb server features and dynamic routing are not supported with this format. However, a huge advantage for this + * format is it can often be served fast and cheap. + */ + STATIC; + + companion object { + fun get(): SiteLayout { + val envValue: String = System.getProperty(PROPERTY_SITE_LAYOUT) ?: KOBWEB.name + return SiteLayout.values().firstOrNull { layout -> layout.name == envValue } + ?: throw KobwebException("Invalid export layout: $envValue, expected one of [${ServerEnvironment.values().joinToString()}]") + } + } + + fun toSystemPropertyParam(): String = "-D${PROPERTY_SITE_LAYOUT}=${this.name}" +} \ No newline at end of file diff --git a/gradle-plugins/application/src/main/kotlin/com/varabyte/kobweb/gradle/application/KobwebApplicationPlugin.kt b/gradle-plugins/application/src/main/kotlin/com/varabyte/kobweb/gradle/application/KobwebApplicationPlugin.kt index 3b8be5c19..3e80046e1 100644 --- a/gradle-plugins/application/src/main/kotlin/com/varabyte/kobweb/gradle/application/KobwebApplicationPlugin.kt +++ b/gradle-plugins/application/src/main/kotlin/com/varabyte/kobweb/gradle/application/KobwebApplicationPlugin.kt @@ -12,6 +12,7 @@ import com.varabyte.kobweb.gradle.application.tasks.KobwebGenerateSiteTask import com.varabyte.kobweb.gradle.application.tasks.KobwebStartTask import com.varabyte.kobweb.gradle.application.tasks.KobwebStopTask import com.varabyte.kobweb.project.KobwebFolder +import com.varabyte.kobweb.server.api.SiteLayout import com.varabyte.kobweb.server.api.ServerEnvironment import com.varabyte.kobweb.server.api.ServerRequest import com.varabyte.kobweb.server.api.ServerRequestsFile @@ -38,6 +39,10 @@ class KobwebApplicationPlugin : Plugin { val env = project.findProperty("kobwebEnv")?.let { ServerEnvironment.valueOf(it.toString()) } ?: ServerEnvironment.DEV + val runLayout = + project.findProperty("kobwebRunLayout")?.let { SiteLayout.valueOf(it.toString()) } ?: SiteLayout.KOBWEB + val exportLayout = + project.findProperty("kobwebExportLayout")?.let { SiteLayout.valueOf(it.toString()) } ?: SiteLayout.KOBWEB val buildTarget = project.findProperty("kobwebBuildTarget")?.let { BuildTarget.valueOf(it.toString()) } ?: if (env == ServerEnvironment.DEV) BuildTarget.DEBUG else BuildTarget.RELEASE @@ -53,10 +58,11 @@ class KobwebApplicationPlugin : Plugin { val kobwebStartTask = run { val reuseServer = project.findProperty("kobwebReuseServer")?.let { it.toString().toBoolean() } ?: true - project.tasks.register("kobwebStart", KobwebStartTask::class.java, env, reuseServer) + project.tasks.register("kobwebStart", KobwebStartTask::class.java, env, runLayout, reuseServer) } project.tasks.register("kobwebStop", KobwebStopTask::class.java) - val kobwebExportTask = project.tasks.register("kobwebExport", KobwebExportTask::class.java, kobwebConfig) + val kobwebExportTask = + project.tasks.register("kobwebExport", KobwebExportTask::class.java, kobwebConfig, env, exportLayout) project.afterEvaluate { project.tasks.named("clean") { diff --git a/gradle-plugins/application/src/main/kotlin/com/varabyte/kobweb/gradle/application/tasks/KobwebExportTask.kt b/gradle-plugins/application/src/main/kotlin/com/varabyte/kobweb/gradle/application/tasks/KobwebExportTask.kt index e67c20d6f..81b8d38eb 100644 --- a/gradle-plugins/application/src/main/kotlin/com/varabyte/kobweb/gradle/application/tasks/KobwebExportTask.kt +++ b/gradle-plugins/application/src/main/kotlin/com/varabyte/kobweb/gradle/application/tasks/KobwebExportTask.kt @@ -4,8 +4,11 @@ package com.varabyte.kobweb.gradle.application.tasks import com.github.kklisura.cdt.launch.ChromeArguments import com.github.kklisura.cdt.launch.ChromeLauncher +import com.github.kklisura.cdt.services.ChromeService import com.varabyte.kobweb.gradle.application.extensions.KobwebConfig import com.varabyte.kobweb.gradle.application.project.site.SiteData +import com.varabyte.kobweb.server.api.SiteLayout +import com.varabyte.kobweb.server.api.ServerEnvironment import com.varabyte.kobweb.server.api.ServerStateFile import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction @@ -13,7 +16,7 @@ import org.jsoup.Jsoup import java.io.File import javax.inject.Inject -abstract class KobwebExportTask @Inject constructor(config: KobwebConfig) : +abstract class KobwebExportTask @Inject constructor(config: KobwebConfig, private val serverEnvironment: ServerEnvironment, private val siteLayout: SiteLayout) : KobwebProjectTask(config, "Export the Kobweb project into a static site") { @OutputDirectory @@ -21,6 +24,28 @@ abstract class KobwebExportTask @Inject constructor(config: KobwebConfig) : return project.layout.projectDirectory.dir(kobwebConfFile.content!!.server.files.prod.siteRoot).asFile } + private fun ChromeService.takeSnapshot(url: String): String { + lateinit var snapshot: String + + val tab = createTab() + val devToolsService = createDevToolsService(tab) + val page = devToolsService.page + val runtime = devToolsService.runtime + page.onLoadEventFired { + val evaluation = runtime.evaluate("document.documentElement.outerHTML") + snapshot = Jsoup.parse(evaluation.result.value.toString()).toString() + devToolsService.close() + } + page.enable() + page.navigate(url) + devToolsService.waitUntilClosed() + + return snapshot + } + + private fun T.toTriple() = Triple(this, this, this) + private fun Triple.map(transform: (T) -> S) = Triple(transform(first), transform(second), transform(third)) + @TaskAction fun execute() { // Sever should be running since "kobwebStart" is a prerequisite for this task @@ -32,6 +57,12 @@ abstract class KobwebExportTask @Inject constructor(config: KobwebConfig) : getSourceFilesJs(), GradleReporter(project.logger) ) + + val (pagesRoot, resourcesRoot, systemRoot) = when(siteLayout) { + SiteLayout.KOBWEB -> Triple("pages", "resources", "system").map { File(getSiteDir(), it) } + SiteLayout.STATIC -> getSiteDir().toTriple() + } + siteData.pages.takeIf { it.isNotEmpty() }?.let { pages -> ChromeLauncher().use { launcher -> // NOTE: Normally "no-sandbox" is NOT recommended for security reasons. However, this option is @@ -45,61 +76,69 @@ abstract class KobwebExportTask @Inject constructor(config: KobwebConfig) : ) pages + .map { it.route } // Skip export routes with dynamic parts, as they are dynamically generated based on their URL // anyway - .filter { !it.route.contains('{') } - .forEach { pageEntry -> - val tab = chromeService.createTab() - val devToolsService = chromeService.createDevToolsService(tab) - val page = devToolsService.page - val runtime = devToolsService.runtime - page.onLoadEventFired { _ -> - val evaluation = runtime.evaluate("document.documentElement.outerHTML") - val filePath = pageEntry.route.substringBeforeLast('/') + "/" + - (pageEntry.route.substringAfterLast('/').takeIf { it.isNotEmpty() } ?: "index") + - ".html" - val prettyHtml = Jsoup.parse(evaluation.result.value.toString()).toString() - File(getSiteDir(), "pages$filePath").run { + .filter { !it.contains('{') } + .toSet() + .forEach { route -> + val snapshot = chromeService.takeSnapshot("http://localhost:$port$route") + + var filePath = route.substringBeforeLast('/') + "/" + + (route.substringAfterLast('/').takeIf { it.isNotEmpty() } ?: "index") + + ".html" + + // Drop the leading slash so we don't confuse File resolve logic + filePath = filePath.drop(1) + pagesRoot + .resolve(filePath) + .run { parentFile.mkdirs() - writeText(prettyHtml) + writeText(snapshot) } - devToolsService.close() - } - page.enable() - page.navigate("http://localhost:$port${pageEntry.route}") - devToolsService.waitUntilClosed() } } } + // Copy resources. + // Note: The "index.html" file that comes from here is auto-generated and useful as a fallback for dynamic + // export layouts but shouldn't be copied over in static layouts as those should only include pages explicitly + // defined by the site. getResourceFilesJsWithRoots().forEach { rootAndFile -> - val relativePath = rootAndFile.relativeFile.toString().substringAfter(getPublicPath()) - // The auto-generated "/index.html" file should be used as a fallback if the user visits an invalid path - val destFile = File( - getSiteDir(), - if (relativePath != "/index.html") "resources$relativePath" else "system$relativePath" - ) - rootAndFile.file.copyTo(destFile, overwrite = true) + // Drop the leading slash so we don't confuse File resolve logic + val relativePath = rootAndFile.relativeFile.toString().substringAfter(getPublicPath()).drop(1) + if (relativePath == "index.html" && siteLayout != SiteLayout.KOBWEB) return@forEach + + (if (relativePath != "index.html") resourcesRoot else systemRoot) + .resolve(relativePath) + .let { destFile -> + rootAndFile.file.copyTo(destFile, overwrite = true) + } } val scriptFile = project.layout.projectDirectory.file(kobwebConf.server.files.dev.script).asFile run { - val destFile = File(getSiteDir(), "system/${scriptFile.name}") + val destFile = systemRoot.resolve(scriptFile.name) scriptFile.copyTo(destFile, overwrite = true) } - val scriptMapFile = File("${scriptFile}.map") - run { - val destFile = File(getSiteDir(), "system/${scriptMapFile.name}") - scriptMapFile.copyTo(destFile, overwrite = true) + if (serverEnvironment == ServerEnvironment.DEV) { + val scriptMapFile = File("${scriptFile}.map") + run { + val destFile = systemRoot.resolve(scriptMapFile.name) + scriptMapFile.copyTo(destFile, overwrite = true) + } } - // The api.jar is not guaranteed to exist -- not every project needs to have API routes defined. - kobwebConf.server.files.dev.api.takeIf { it.isNotBlank() }?.let { apiFile -> - val apiJarFile = project.layout.projectDirectory.file(apiFile).asFile - if (apiJarFile.exists()) { - val destFile = File(getSiteDir(), "system/${apiJarFile.name}") - apiJarFile.copyTo(destFile, overwrite = true) + // Kobweb servers are only supported by the Kobweb layout + if (siteLayout == SiteLayout.KOBWEB) { + // The api.jar is not guaranteed to exist -- not every project needs to have API routes defined. + kobwebConf.server.files.dev.api.takeIf { it.isNotBlank() }?.let { apiFile -> + val apiJarFile = project.layout.projectDirectory.file(apiFile).asFile + if (apiJarFile.exists()) { + val destFile = systemRoot.resolve(apiJarFile.name) + apiJarFile.copyTo(destFile, overwrite = true) + } } } } diff --git a/gradle-plugins/application/src/main/kotlin/com/varabyte/kobweb/gradle/application/tasks/KobwebStartTask.kt b/gradle-plugins/application/src/main/kotlin/com/varabyte/kobweb/gradle/application/tasks/KobwebStartTask.kt index cb69aed07..8082ce9f5 100644 --- a/gradle-plugins/application/src/main/kotlin/com/varabyte/kobweb/gradle/application/tasks/KobwebStartTask.kt +++ b/gradle-plugins/application/src/main/kotlin/com/varabyte/kobweb/gradle/application/tasks/KobwebStartTask.kt @@ -2,6 +2,7 @@ package com.varabyte.kobweb.gradle.application.tasks +import com.varabyte.kobweb.server.api.SiteLayout import com.varabyte.kobweb.server.api.ServerEnvironment import com.varabyte.kobweb.server.api.ServerStateFile import org.gradle.api.GradleException @@ -20,6 +21,7 @@ import javax.inject.Inject */ abstract class KobwebStartTask @Inject constructor( private val env: ServerEnvironment, + private val siteLayout: SiteLayout, private val reuseServer: Boolean ) : KobwebTask("Start a Kobweb server") { @@ -54,6 +56,7 @@ abstract class KobwebStartTask @Inject constructor( val processParams = arrayOf( "$javaHome/bin/java", env.toSystemPropertyParam(), + siteLayout.toSystemPropertyParam(), // See: https://ktor.io/docs/development-mode.html#system-property "-Dio.ktor.development=${env == ServerEnvironment.DEV}", "-jar",