Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f18d7d9
Adds devserve to viaduct application.
Nov 14, 2025
f373c09
Added devserve.
Nov 14, 2025
e59ecf6
Added devserve.
Nov 17, 2025
641ae3c
Makes sure graphiql is working.
Nov 17, 2025
2c73b43
Added devserve.
Nov 17, 2025
a35046d
Fixes settings.
Nov 17, 2025
bcf50ee
Changed structure to make it build properly.
Nov 18, 2025
c52bf88
Fixes graphiql code.
Nov 18, 2025
4cb2ce3
Added devserve.
Nov 18, 2025
50cd501
Fixed demoapps.
Nov 18, 2025
304c388
Got continuous mode working.
Nov 18, 2025
59cb6f0
Merge branch 'main' into add_devserv_mode
fireboy1919 Nov 18, 2025
a4e0e7b
Refactor devserve implementation and terminology
Nov 19, 2025
ee8d756
fix(devserve): resolve INCLUDED version and Gradle 9.x compatibility …
Nov 25, 2025
0c216a0
Merge branch 'main' into add_devserv_mode
fireboy1919 Dec 1, 2025
9114cb9
fix: restore and configure coverage verification task
Dec 1, 2025
6be21d3
refactor(api): rename @ViaductApplication to @ViaductConfiguration
Dec 1, 2025
836c93e
feat(devserve): add ViaductDevServeProvider for all demo apps
Dec 1, 2025
eca262b
refactor(devserve): use GraphiQL from service-wiring module
Dec 5, 2025
74be977
refactor(starwars): remove devserve integration
Dec 5, 2025
4948594
refactor: rename devserve to serve throughout codebase
Dec 5, 2025
7ad5fdd
refactor(serve): make DefaultViaductFactory internal with automatic f…
Dec 5, 2025
5a5d9f7
feat(service-wiring): add GraphiQL HTML resources with Viaduct custom…
Dec 5, 2025
5857baa
test(serve): add JS file serving test and improve resource loading
Dec 8, 2025
039ce80
feat(demoapps): add micronaut-starter with serve-only DI support
Dec 8, 2025
65d87fa
refactor(micronaut-starter): use limited package scanning for faster …
Dec 8, 2025
823bff9
feat(serve): add ServeServer entry point and enable zero-config for c…
Dec 8, 2025
401ca5a
docs(serve): add DI configuration documentation and improve warnings
Dec 9, 2025
1ce921b
docs(service-wiring): add integration examples for GraphiQL HTML gene…
Dec 9, 2025
aa787d2
test(serve): add introspection response validation tests
Dec 9, 2025
e8c7887
refactor(graphiql): remove unnecessary introspection-patch.js
Dec 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
plugins {
`kotlin-dsl`
}

repositories {
mavenCentral()
}
155 changes: 155 additions & 0 deletions buildSrc/src/main/kotlin/viaduct/devserve/GraphiQLHtmlCustomizer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package viaduct.devserve

import java.io.File
import java.net.URL

/**
* Customizes the official GraphiQL HTML for use with Viaduct DevServe.
*
* Downloads the base HTML from a specific GraphiQL release and applies Viaduct customizations
* by parsing the HTML structure and inserting our code at appropriate locations.
* This is more robust than text-based patches as it adapts to HTML structure changes.
*/
class GraphiQLHtmlCustomizer(
private val sourceUrl: String,
private val outputFile: File
) {
/**
* Downloads and customizes the GraphiQL HTML, writing the result to the output file.
*/
fun customize() {
val html = downloadHtml()
val customized = applyCustomizations(html)
writeOutput(customized)
}

private fun downloadHtml(): String {
val tempFile = File.createTempFile("graphiql", ".html")
try {
URL(sourceUrl).openStream().use { input ->
tempFile.outputStream().use { output ->
input.copyTo(output)
}
}
return tempFile.readText()
} finally {
tempFile.delete()
}
}

private fun applyCustomizations(html: String): String {
var customized = html

// 1. Update title
customized = customized.replace(
Regex("<title>.*?</title>"),
"<title>GraphiQL - Viaduct DevServe</title>"
)

// 2. Update copyright
customized = customized.replace(
"Copyright (c) 2025 GraphQL Contributors",
"Copyright (c) 2025 Airbnb, Inc."
)

// 3. Change demo endpoint to /graphql
customized = customized.replace(
"url: 'https://countries.trevorblades.com'",
"url: '/graphql'"
)

// 4. Find the module script and inject our customizations
val moduleScriptPattern = Regex(
"""(<script type="module">)(.*?)(</script>)""",
RegexOption.DOT_MATCHES_ALL
)

val moduleScriptMatch = moduleScriptPattern.find(customized)
if (moduleScriptMatch != null) {
val (opening, scriptContent, closing) = moduleScriptMatch.destructured

// Add our imports at the beginning of the module script
val viaductImports = """
import { loadJSX } from '/js/jsx-loader.js';
import { createPatchedFetcher } from '/js/introspection-patch.js';
"""

// Wrap the fetcher creation with our patch
var modifiedScript = scriptContent.replace(
Regex("""const fetcher = createGraphiQLFetcher\(\{[\s\S]*?\}\);"""),
"""const baseFetcher = createGraphiQLFetcher({
url: '/graphql',
});
const fetcher = createPatchedFetcher(baseFetcher);"""
)

// Modify the plugin initialization to load our Global ID plugin
modifiedScript = modifiedScript.replace(
"const plugins = [HISTORY_PLUGIN, explorerPlugin()];",
"""// Load Viaduct plugins asynchronously
async function loadPlugins() {
try {
const pluginModule = await loadJSX('/js/global-id-plugin.jsx');
const createGlobalIdPlugin = pluginModule.createGlobalIdPlugin;
const globalIdPlugin = createGlobalIdPlugin(React);
return [HISTORY_PLUGIN, explorerPlugin(), globalIdPlugin];
} catch (error) {
console.error('Failed to load Viaduct Global ID plugin:', error);
return [HISTORY_PLUGIN, explorerPlugin()];
}
}"""
)

// Replace the App rendering to be async and use our plugins
modifiedScript = modifiedScript.replace(
Regex("""function App\(\)[\s\S]*?root\.render\(React\.createElement\(App\)\);"""),
"""async function initGraphiQL() {
const plugins = await loadPlugins();
const explorer = plugins.find(p => p.title === 'Explorer');
const defaultQuery = `# Welcome to Viaduct DevServe!
#
# Start typing your GraphQL query here.
# Press Ctrl+Space for autocomplete.
# Click the Docs button to explore the schema.
# Use the Global ID Utils plugin (key icon) to encode/decode Viaduct Global IDs.

query {
# Your query here
}
`;

function App() {
return React.createElement(GraphiQL, {
fetcher,
plugins,
visiblePlugin: explorer,
defaultQuery,
defaultEditorToolsVisibility: true,
});
}

const container = document.getElementById('graphiql');
const root = ReactDOM.createRoot(container);
root.render(React.createElement(App));
}

initGraphiQL();"""
)

// Reconstruct the script with our imports
val newModuleScript = opening + viaductImports + modifiedScript + closing
customized = customized.replace(moduleScriptMatch.value, newModuleScript)
} else {
throw IllegalStateException(
"Could not find module script in GraphiQL HTML. HTML structure may have changed."
)
}

return customized
}

private fun writeOutput(html: String) {
outputFile.parentFile.mkdirs()
outputFile.writeText(html)
}
}
3 changes: 0 additions & 3 deletions demoapps/cli-starter/settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

val viaductVersion: String by settings

// When part of composite build, use local gradle-plugins
// When standalone, use Maven Central (only after version is published)
pluginManagement {
if (gradle.parent != null) {
includeBuild("../../gradle-plugins")
Expand All @@ -22,7 +20,6 @@ dependencyResolutionManagement {
}
versionCatalogs {
create("libs") {
// This injects a dynamic value that your TOML can reference.
version("viaduct", viaductVersion)
}
}
Expand Down
3 changes: 0 additions & 3 deletions demoapps/jetty-starter/settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ rootProject.name = "viaduct-jetty-starter"

val viaductVersion: String by settings

// When part of composite build, use local gradle-plugins
// When standalone, use Maven Central (only after version is published)
pluginManagement {
if (gradle.parent != null) {
includeBuild("../../gradle-plugins")
Expand All @@ -22,7 +20,6 @@ dependencyResolutionManagement {
}
versionCatalogs {
create("libs") {
// This injects a dynamic value that your TOML can reference.
version("viaduct", viaductVersion)
}
}
Expand Down
3 changes: 0 additions & 3 deletions demoapps/ktor-starter/settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ rootProject.name = "viaduct-ktor-starter"

val viaductVersion: String by settings

// When part of composite build, use local gradle-plugins
// When standalone, use Maven Central (only after version is published)
pluginManagement {
if (gradle.parent != null) {
includeBuild("../../gradle-plugins")
Expand All @@ -22,7 +20,6 @@ dependencyResolutionManagement {
}
versionCatalogs {
create("libs") {
// This injects a dynamic value that your TOML can reference.
version("viaduct", viaductVersion)
}
}
Expand Down
3 changes: 0 additions & 3 deletions demoapps/starwars/settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
val viaductVersion: String by settings

// When part of composite build, use local gradle-plugins
// When standalone, use Maven Central (only after version is published)
pluginManagement {
if (gradle.parent != null) {
includeBuild("../../gradle-plugins")
Expand All @@ -20,7 +18,6 @@ dependencyResolutionManagement {
}
versionCatalogs {
create("libs") {
// This injects a dynamic value that your TOML can reference.
version("viaduct", viaductVersion)
}
}
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.example.starwars.service.viaduct

import viaduct.service.BasicViaductFactory
import viaduct.service.SchemaRegistrationInfo
import viaduct.service.TenantRegistrationInfo
import viaduct.service.api.Viaduct
import viaduct.service.api.ViaductApplication
import viaduct.service.api.ViaductFactory
import viaduct.service.api.spi.TenantCodeInjector
import viaduct.service.toSchemaScopeInfo

/**
* ViaductFactory for the StarWars demo application.
*
* This factory creates the Viaduct instance with the appropriate configuration
* and tenant code injector. It supports two modes:
*
* 1. Production mode (via ViaductConfiguration): Micronaut provides the injector
* 2. DevServe mode (no-arg constructor): Starts Micronaut and gets the injector
*/
@ViaductApplication
class StarWarsViaductFactory : ViaductFactory {
private val tenantCodeInjector: TenantCodeInjector

/**
* No-arg constructor for devserve mode.
* Starts Micronaut ApplicationContext and obtains the TenantCodeInjector from it.
*/
constructor() {
// Start Micronaut ApplicationContext
val contextClass = Class.forName("io.micronaut.context.ApplicationContext")
val runMethod = contextClass.getMethod("run")
val context = runMethod.invoke(null) // ApplicationContext.run() is static

// Get the MicronautTenantCodeInjector bean from the context
val getBeanMethod = contextClass.getMethod("getBean", Class::class.java)
val injectorClass = Class.forName("com.example.starwars.service.viaduct.MicronautTenantCodeInjector")
tenantCodeInjector = getBeanMethod.invoke(context, injectorClass) as TenantCodeInjector
}

/**
* Constructor for production mode.
* Used by ViaductConfiguration with Micronaut DI providing the injector.
*
* @param tenantCodeInjector The dependency injection provider for tenant code.
*/
constructor(tenantCodeInjector: TenantCodeInjector) {
this.tenantCodeInjector = tenantCodeInjector
}

override fun createViaduct(): Viaduct {
return BasicViaductFactory.create(
schemaRegistrationInfo = SchemaRegistrationInfo(
scopes = listOf(
DEFAULT_SCHEMA_ID.toSchemaScopeInfo(),
EXTRAS_SCHEMA_ID.toSchemaScopeInfo(),
),
packagePrefix = "com.example.starwars",
resourcesIncluded = ".*\\.graphqls"
),
tenantRegistrationInfo = TenantRegistrationInfo(
tenantPackagePrefix = "com.example.starwars",
tenantCodeInjector = tenantCodeInjector
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@ package com.example.starwars.service.viaduct

import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory
import viaduct.service.BasicViaductFactory
import viaduct.service.SchemaRegistrationInfo
import viaduct.service.TenantRegistrationInfo
import viaduct.service.api.SchemaId
import viaduct.service.api.Viaduct
import viaduct.service.toSchemaScopeInfo

const val DEFAULT_SCOPE_ID = "default"
const val EXTRAS_SCOPE_ID = "extras"
Expand All @@ -20,19 +16,9 @@ class ViaductConfiguration(
) {
@Bean
fun providesViaduct(): Viaduct {
return BasicViaductFactory.create(
schemaRegistrationInfo = SchemaRegistrationInfo(
scopes = listOf(
DEFAULT_SCHEMA_ID.toSchemaScopeInfo(),
EXTRAS_SCHEMA_ID.toSchemaScopeInfo(),
),
packagePrefix = "com.example.starwars",
resourcesIncluded = ".*\\.graphqls"
),
tenantRegistrationInfo = TenantRegistrationInfo(
tenantPackagePrefix = "com.example.starwars",
tenantCodeInjector = micronautTenantCodeInjector
)
)
// Use StarWarsViaductFactory to create the Viaduct instance
// This ensures both production and devserve use the same factory
val factory = StarWarsViaductFactory(micronautTenantCodeInjector)
return factory.createViaduct()
}
}
Loading
Loading