diff --git a/build-system/conventions/src/main/kotlin/convention.core-fixtures.gradle.kts b/build-system/conventions/src/main/kotlin/convention.core-fixtures.gradle.kts index e80a019391..fdfcae8327 100644 --- a/build-system/conventions/src/main/kotlin/convention.core-fixtures.gradle.kts +++ b/build-system/conventions/src/main/kotlin/convention.core-fixtures.gradle.kts @@ -2,13 +2,19 @@ import com.android.build.gradle.LibraryExtension import io.gitlab.arturbosch.detekt.Detekt import io.gitlab.arturbosch.detekt.extensions.DetektExtension import tasks.docs.ExtractCodeSnippetsTask +import tasks.docs.ValidateCoreDocumentationTask import utils.baseDetektConfigDirPath import utils.baseDetektConfigPath +import utils.getDocsTemplateDir import utils.isAndroidLib +import utils.isComposeLib import utils.withVersionCatalogs val docsDirPath = "docs" +val coreDocsTemplateDir = getDocsTemplateDir().resolve( + if (isComposeLib()) "compose-template" else "xml-template", +) val kotlinCompilerDependencies = configurations.create("kotlinCompilerDependencies") @@ -30,19 +36,31 @@ tasks.register("collectCodeSnippets") { val namespace = project.extensions.getByType(LibraryExtension::class.java).namespace.orEmpty() xmlNamespace.set(namespace) } - outputKotlinDir.set(layout.buildDirectory.dir(docsDirPath)) - outputXmlDir.set(layout.buildDirectory.dir(docsDirPath)) - outputMeta.set(layout.buildDirectory.file("$docsDirPath/meta.json")) + outputKotlinDir.set(layout.buildDirectory.dir("$docsDirPath/assets/examples/kotlin")) + outputXmlDir.set(layout.buildDirectory.dir("$docsDirPath/assets/examples/xml")) + outputMeta.set(layout.buildDirectory.file("$docsDirPath/meta/samples.json")) } val docsJar = tasks.register("docsJar") { archiveClassifier.set("docs") - dependsOn("collectCodeSnippets") + dependsOn("collectCodeSnippets", "validateCoreDocumentation") from(layout.buildDirectory.dir(docsDirPath)) { into("META-INF/sdds-docs") } + from(coreDocsTemplateDir) { + include("structure.json") + include("docs/**/*.md") + into("META-INF/sdds-docs") + } +} + +tasks.register("validateCoreDocumentation") { + group = "verification" + description = "Проверяет соответствие structure.json Core markdown-шаблонам" + structureFile.set(coreDocsTemplateDir.resolve("structure.json")) + docsDirectory.set(coreDocsTemplateDir.resolve("docs")) } val docsVariantAttr: Attribute = Attribute.of("com.sdds.docs.variant", String::class.java) @@ -53,7 +71,7 @@ val docsElements by configurations.creating { attributes { attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME)) attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.DOCUMENTATION)) - attribute(docsVariantAttr, "snippets") + attribute(docsVariantAttr, "templates") } outgoing.artifact(docsJar) @@ -79,4 +97,4 @@ configure { if (file(integrationConfig).exists()) { config = files(project.baseDetektConfigPath(), integrationConfig) } -} \ No newline at end of file +} diff --git a/build-system/conventions/src/main/kotlin/convention.documentation-compose.gradle.kts b/build-system/conventions/src/main/kotlin/convention.documentation-compose.gradle.kts deleted file mode 100644 index c6605d1a5b..0000000000 --- a/build-system/conventions/src/main/kotlin/convention.documentation-compose.gradle.kts +++ /dev/null @@ -1,17 +0,0 @@ -import extensions.docs.DocusaurusExtension - -plugins { - id("convention.documentation") - id("convention.compose") -} - -extensions.configure("docusaurus") { - components.set(layout.projectDirectory.file("../.sdds/config-info-compose.json")) - -} - -dependencies { - "docsSnippets"("integration-core:uikit-compose-fixtures:unspecified:docs@jar") - "implementation"("sdds-core:docs") - "testImplementation"("integration-core:uikit-compose-fixtures") -} \ No newline at end of file diff --git a/build-system/conventions/src/main/kotlin/convention.documentation-view.gradle.kts b/build-system/conventions/src/main/kotlin/convention.documentation-view.gradle.kts deleted file mode 100644 index 162eb13b1a..0000000000 --- a/build-system/conventions/src/main/kotlin/convention.documentation-view.gradle.kts +++ /dev/null @@ -1,16 +0,0 @@ -import extensions.docs.DocusaurusExtension -import org.gradle.kotlin.dsl.dependencies - -plugins { - id("convention.documentation") -} - -extensions.configure("docusaurus") { - components.set(layout.projectDirectory.file("../config-info-view-system.json")) -} - -dependencies { - "implementation"("sdds-core:docs") - "docsSnippets"("integration-core:uikit-fixtures:unspecified:docs@jar") - "testImplementation"("integration-core:uikit-fixtures") -} \ No newline at end of file diff --git a/build-system/conventions/src/main/kotlin/convention.documentation.gradle.kts b/build-system/conventions/src/main/kotlin/convention.documentation.gradle.kts deleted file mode 100644 index f1f26d82c0..0000000000 --- a/build-system/conventions/src/main/kotlin/convention.documentation.gradle.kts +++ /dev/null @@ -1,92 +0,0 @@ -import com.android.build.gradle.LibraryExtension -import extensions.docs.DocusaurusExtension -import extensions.docs.FixturesExtension -import io.gitlab.arturbosch.detekt.Detekt -import tasks.docs.UnzipCodeSnippetsTask -import org.gradle.api.attributes.Attribute -import tasks.docs.ExtractCodeSnippetsTask -import utils.isAndroidLib - -plugins { - id("convention.android-lib") - id("convention.docusaurus") - id("com.google.devtools.ksp") -} - -val kotlinCompilerDependencies = configurations.create("kotlinCompilerDependencies") - -dependencies { - add(kotlinCompilerDependencies.name, "org.jetbrains.kotlin:kotlin-compiler-embeddable:2.1.10") -} - -val kotlinCompilerClassPath = configurations.create("kotlinCompilerClassPath") { - extendsFrom(kotlinCompilerDependencies) -} - -val docsVariantAttr: Attribute = Attribute.of("com.sdds.docs.variant", String::class.java) - -val docsPath = "docs" - -val extension = extensions.create("fixtures", FixturesExtension::class).apply { - kotlinSnippetsDir.convention(layout.buildDirectory.dir(docsPath)) - xmlSnippetsDir.convention(layout.buildDirectory.dir(docsPath)) - metaFile.convention(layout.buildDirectory.file("${docsPath}/meta.json")) -} - -extensions.configure("docusaurus") { - snippetsDir.set(layout.buildDirectory.dir(docsPath)) -} - -val collectSnippets = tasks.register("collectCodeSnippets") { - group = "documentation" - description = "Извлекает код внутри codeSnippet/composableCodeSnippet из функций @DocSample" - - kotlinCompiler.from(kotlinCompilerClassPath) - - if (isAndroidLib()) { - val namespace = project.extensions.getByType(LibraryExtension::class.java).namespace.orEmpty() - xmlNamespace.set(namespace) - } - outputKotlinDir.set(extension.kotlinSnippetsDir) - outputXmlDir.set(extension.xmlSnippetsDir) - outputMeta.set(extension.metaFile) - dependsOn(unzipSnippets) -} - -val unzipSnippets = tasks.register("unzipCoreSnippets") { - group = "documentation" - description = "Извлекает примеры кода core компонентов" - - snippetsOutDir.set(layout.buildDirectory.dir(docsPath)) - docsArtifacts.from(configurations.named("docsSnippets")) -} - -tasks.named("docusaurusGenerate").configure { - dependsOn(collectSnippets) -} - -tasks.withType().configureEach { - exclude("**/*Samples.kt") -} - -val docsSnippets by configurations.creating { - isCanBeResolved = true - isCanBeConsumed = false - description = "Артефакты с примерами кода" - attributes { - attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME)) - attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.DOCUMENTATION)) - attribute(docsVariantAttr, "snippets") - } -} - -afterEvaluate { - ksp { - arg("packageName", project.extensions.getByType(LibraryExtension::class.java).namespace.orEmpty()) - } -} - -dependencies { - "implementation"("sdds-core:docs") - ksp("sdds-core:docs") -} \ No newline at end of file diff --git a/build-system/conventions/src/main/kotlin/convention.docusaurus.gradle.kts b/build-system/conventions/src/main/kotlin/convention.docusaurus.gradle.kts index 74cf00c85e..9469d0ab6a 100644 --- a/build-system/conventions/src/main/kotlin/convention.docusaurus.gradle.kts +++ b/build-system/conventions/src/main/kotlin/convention.docusaurus.gradle.kts @@ -73,7 +73,9 @@ val generateInstanceTask by tasks.register("docusaurusGenerate") { copy { duplicatesStrategy = DuplicatesStrategy.INCLUDE - from(overrideDocsDir) + from(overrideDocsDir) { + exclude("structure.json") + } into(destinationDir) } diff --git a/build-system/conventions/src/main/kotlin/convention.integration-compose.gradle.kts b/build-system/conventions/src/main/kotlin/convention.integration-compose.gradle.kts deleted file mode 100644 index 7e23690502..0000000000 --- a/build-system/conventions/src/main/kotlin/convention.integration-compose.gradle.kts +++ /dev/null @@ -1,43 +0,0 @@ -import tasks.integration.ComponentsTarget -import io.gitlab.arturbosch.detekt.extensions.DetektExtension -import org.gradle.kotlin.dsl.register -import tasks.integration.GenerateComponentsDictionary -import tasks.integration.Scheme -import utils.baseDetektConfigDirPath -import utils.baseDetektConfigPath - - -tasks.register("generateComposeIntegration") { - val configuredConfigPath = properties["integration.compose.config-path"]?.toString() - ?: throw GradleException("integration.compose.config-path is not specified") - val configuredConfigFile = rootProject.file(configuredConfigPath) - val sddsConfigFile = configuredConfigFile.parentFile.resolve(".sdds/${configuredConfigFile.name}") - configInputFile.set( - if (configuredConfigFile.exists()) configuredConfigFile else sddsConfigFile - ) - packageName.set( - properties["integration.compose.package-name"]?.toString() - ?: throw GradleException("integration.compose.package-name is not specified") - ) - themeAlias.set(properties["theme-alias"]?.toString()) - multiplatform.set( - providers.gradleProperty("integration.compose.multiplatform") - .map { it.toBooleanStrict() } - .orElse(provider { plugins.hasPlugin("org.jetbrains.kotlin.multiplatform") }), - ) - scheme.set( - when (properties["integration.compose.scheme"]?.toString()) { - "V2" -> Scheme.V2 - else -> Scheme.V1 - } - ) - - target.set(ComponentsTarget.COMPOSE) -} - -configure { - val integrationConfig = "${project.baseDetektConfigDirPath()}/integration-config.yml" - if (file(integrationConfig).exists()) { - config = files(project.baseDetektConfigPath(), integrationConfig) - } -} diff --git a/build-system/conventions/src/main/kotlin/convention.integration-detekt.gradle.kts b/build-system/conventions/src/main/kotlin/convention.integration-detekt.gradle.kts new file mode 100644 index 0000000000..b835f5c803 --- /dev/null +++ b/build-system/conventions/src/main/kotlin/convention.integration-detekt.gradle.kts @@ -0,0 +1,16 @@ +import io.gitlab.arturbosch.detekt.Detekt +import io.gitlab.arturbosch.detekt.extensions.DetektExtension +import utils.baseDetektConfigDirPath +import utils.baseDetektConfigPath + +tasks.withType().configureEach { + exclude("**/*Samples.kt") +} + +configure { + val integrationConfig = "${project.baseDetektConfigDirPath()}/integration-config.yml" + + if (file(integrationConfig).exists()) { + config = files(project.baseDetektConfigPath(), integrationConfig) + } +} diff --git a/build-system/conventions/src/main/kotlin/convention.integration-view.gradle.kts b/build-system/conventions/src/main/kotlin/convention.integration-view.gradle.kts deleted file mode 100644 index 5803165f1f..0000000000 --- a/build-system/conventions/src/main/kotlin/convention.integration-view.gradle.kts +++ /dev/null @@ -1,42 +0,0 @@ -import com.diffplug.gradle.spotless.SpotlessExtension -import io.gitlab.arturbosch.detekt.extensions.DetektExtension -import org.gradle.kotlin.dsl.register -import tasks.integration.ComponentsTarget -import tasks.integration.GenerateComponentsDictionary -import tasks.integration.Scheme -import utils.baseDetektConfigDirPath -import utils.baseDetektConfigPath -import utils.withVersionCatalogs - -tasks.register("generateViewIntegration") { - val currentScheme = properties["integration.view.scheme"] - ?.toString() - ?.let { Scheme.valueOf(it) } - ?: Scheme.V1 - scheme.set(currentScheme) - val sourceProject = when(currentScheme) { - Scheme.V1 -> rootProject - Scheme.V2 -> project - } - themeAlias.set(properties["theme-alias"]?.toString()) - configInputFile.set( - sourceProject.file( - properties["integration.view.config-path"] - ?: throw GradleException("integration.view.config-path is not specified") - ) - ) - packageName.set( - properties["integration.view.package-name"]?.toString() - ?: throw GradleException("integration.view.package-name is not specified") - ) - target.set(ComponentsTarget.XML) - -} - -configure { - val integrationConfig = "${project.baseDetektConfigDirPath()}/integration-config.yml" - - if (file(integrationConfig).exists()) { - config = files(project.baseDetektConfigPath(), integrationConfig) - } -} \ No newline at end of file diff --git a/build-system/conventions/src/main/kotlin/convention.spotless.gradle.kts b/build-system/conventions/src/main/kotlin/convention.spotless.gradle.kts index 8102c9cb26..3ec4007c30 100644 --- a/build-system/conventions/src/main/kotlin/convention.spotless.gradle.kts +++ b/build-system/conventions/src/main/kotlin/convention.spotless.gradle.kts @@ -1,6 +1,5 @@ import com.diffplug.gradle.spotless.SpotlessExtension import com.diffplug.gradle.spotless.SpotlessPlugin -import utils.isSandboxIntegrationModule import utils.withVersionCatalogs apply() @@ -15,15 +14,10 @@ configure { kotlin { target("**/*.kt") - targetExclude("**/build/**", "**/buildSrc/**", "**/.*") + targetExclude("**/build/**", "**/buildSrc/**", "**/.sdds/**", "**/.*") trimTrailingWhitespace() indentWithSpaces() endWithNewline() - val maxLineLengthValue = if (project.isSandboxIntegrationModule()) { - "disabled" - } else { - 120 - } withVersionCatalogs { ktlint(versions.staticAnalysis.ktlint.get()) .editorConfigOverride( @@ -34,7 +28,7 @@ configure { "ktlint_standard_package-name" to "disabled", "ktlint_standard_enum-entry-name-case" to "disabled", "ktlint_standard_filename" to "disabled", - "max_line_length" to maxLineLengthValue, + "max_line_length" to 120, ), ) } diff --git a/build-system/conventions/src/main/kotlin/tasks/docs/ValidateCoreDocumentationTask.kt b/build-system/conventions/src/main/kotlin/tasks/docs/ValidateCoreDocumentationTask.kt new file mode 100644 index 0000000000..c899267943 --- /dev/null +++ b/build-system/conventions/src/main/kotlin/tasks/docs/ValidateCoreDocumentationTask.kt @@ -0,0 +1,88 @@ +package tasks.docs + +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.jetbrains.kotlin.com.google.gson.JsonElement +import org.jetbrains.kotlin.com.google.gson.JsonParser +import java.io.File + +/** Validates that Core documentation structure explicitly lists every markdown template. */ +@CacheableTask +abstract class ValidateCoreDocumentationTask : DefaultTask() { + + /** ADR-0003 navigation structure. */ + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val structureFile: RegularFileProperty + + /** Root containing Core markdown templates. */ + @get:InputDirectory + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val docsDirectory: DirectoryProperty + + /** Verifies that structure paths and markdown templates form the same set. */ + @TaskAction + fun validate() { + val structure = structureFile.get().asFile + val docs = docsDirectory.get().asFile + val root = JsonParser.parseString(structure.readText()) + val navigation = root.takeIf(JsonElement::isJsonObject) + ?.asJsonObject + ?.get("navigation") + ?.takeIf(JsonElement::isJsonArray) + ?: throw GradleException("${structure.absolutePath} must contain a navigation array") + val declared = linkedSetOf() + navigation.asJsonArray.forEach { collectPaths(it, declared, structure) } + val actual = docs.walkTopDown() + .filter { it.isFile && it.extension == "md" } + .map { it.relativeTo(docs).invariantSeparatorsPath } + .toSet() + val missing = declared - actual + val unlisted = actual - declared + if (missing.isNotEmpty() || unlisted.isNotEmpty()) { + throw GradleException( + buildString { + append("Core documentation structure does not match markdown templates.") + if (missing.isNotEmpty()) append(" Missing: ${missing.sorted()}.") + if (unlisted.isNotEmpty()) append(" Unlisted: ${unlisted.sorted()}.") + }, + ) + } + } + + private fun collectPaths(element: JsonElement, result: MutableSet, structure: File) { + if (!element.isJsonObject) { + throw GradleException("${structure.absolutePath} contains a non-object navigation entry") + } + val node = element.asJsonObject + node.get("path")?.let { path -> + val value = path.takeIf(JsonElement::isJsonPrimitive)?.asString.orEmpty() + if (!isSafeMarkdownPath(value)) { + throw GradleException("${structure.absolutePath} contains an invalid markdown path: $value") + } + result += value + } + node.get("items")?.let { items -> + if (!items.isJsonArray) { + throw GradleException("${structure.absolutePath} contains non-array navigation items") + } + items.asJsonArray.forEach { collectPaths(it, result, structure) } + } + } + + private fun isSafeMarkdownPath(path: String): Boolean { + val segments = path.replace('\\', '/').split('/') + return path.isNotBlank() && + !path.startsWith("/") && + path.endsWith(".md") && + segments.none { it.isBlank() || it == "." || it == ".." } + } +} diff --git a/build-system/conventions/src/main/kotlin/tasks/integration/GenerateComponentsDictionary.kt b/build-system/conventions/src/main/kotlin/tasks/integration/GenerateComponentsDictionary.kt deleted file mode 100644 index db659128ff..0000000000 --- a/build-system/conventions/src/main/kotlin/tasks/integration/GenerateComponentsDictionary.kt +++ /dev/null @@ -1,142 +0,0 @@ -package tasks.integration - -import org.gradle.api.DefaultTask -import org.gradle.api.GradleException -import org.gradle.api.file.RegularFileProperty -import org.gradle.api.provider.Property -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.InputFile -import org.gradle.api.tasks.TaskAction -import org.jetbrains.kotlin.com.google.gson.GsonBuilder -import java.io.File - -enum class ComponentsTarget { - COMPOSE, - XML, -} - -enum class Scheme { - V1, - V2, -} - -abstract class GenerateComponentsDictionary : DefaultTask() { - - @get:InputFile - abstract val configInputFile: RegularFileProperty - - @get:Input - abstract val packageName: Property - - @get:Input - abstract val themeAlias: Property - - @get:Input - abstract val target: Property - - @get:Input - abstract val scheme: Property - - @get:Input - abstract val multiplatform: Property - - init { - group = "sandbox" - } - - @TaskAction - fun generate() { - val gson = GsonBuilder().setPrettyPrinting().create() - - val configFile = configInputFile.get().asFile - .takeIf { it.exists() } - ?.readText().orEmpty() - if (configFile.isBlank()) { - logger.warn("config file is empty or do not exists") - return - } - - val config = gson.fromJson(configFile, Config::class.java) - - val pkg = packageName.orNull ?: "com.sdds.generated" - val target = target.get() ?: throw GradleException("Property target must be specified") - val themeAlias = themeAlias.get() ?: throw GradleException("Property themeAlias must be specified") - val scheme = scheme.get() ?: Scheme.V1 - val multiplatform = multiplatform.getOrElse(false) - // Determine output dir from main source set root and packageName - val mainRoot = resolveMainSourceRoot(multiplatform) - val pkgPath = pkg.replace('.', File.separatorChar) - cleanupStaleSourceRoots(pkgPath, mainRoot) - val packageDir = File(mainRoot, pkgPath) - if (!packageDir.exists()) packageDir.mkdirs() - - val generator = when(target) { - ComponentsTarget.COMPOSE -> { - ComposeComponentsGenerator( - config = config, - packageName = pkg, - packageDir = packageDir, - scheme = scheme, - themeAlias = themeAlias, - multiplatform = multiplatform, - ) - } - ComponentsTarget.XML -> XmlComponentsGenerator(config, pkg, packageDir, scheme) - } - - generator.generate() - } - - private fun resolveMainSourceRoot(multiplatform: Boolean): File { - // Prefer Kotlin sources, then Java. Fallback to build/generated if none exist. - val projectDir = project.layout.projectDirectory.asFile - val commonKotlinDir = File(projectDir, "src/commonMain/kotlin") - val kotlinDir = File(projectDir, "src/main/kotlin") - val javaDir = File(projectDir, "src/main/java") - return when { - multiplatform -> commonKotlinDir - kotlinDir.exists() -> kotlinDir - javaDir.exists() -> javaDir - else -> project.layout.buildDirectory.dir("generated/sdds").get().asFile - } - } - - private fun cleanupStaleSourceRoots( - packagePath: String, - activeRoot: File, - ) { - val projectDir = project.layout.projectDirectory.asFile - listOf( - File(projectDir, "src/commonMain/kotlin"), - File(projectDir, "src/main/kotlin"), - File(projectDir, "src/main/java"), - ) - .filterNot { it == activeRoot } - .map { File(it, packagePath) } - .filter { it.exists() } - .forEach { packageDir -> - val sourceRoot = generateSequence(packageDir) { it.parentFile } - .first { it.name == "kotlin" || it.name == "java" } - packageDir.deleteRecursively() - deleteEmptyParents(packageDir.parentFile, sourceRoot) - } - } - - private fun deleteEmptyParents( - directory: File?, - stopAt: File, - ) { - var current = directory - while (current != null && current != stopAt) { - if (current.listFiles()?.isEmpty() == true) { - current.delete() - current = current.parentFile - } else { - return - } - } - if (stopAt.listFiles()?.isEmpty() == true) { - stopAt.delete() - } - } -} diff --git a/build-system/conventions/src/main/kotlin/utils/BuildUtils.kt b/build-system/conventions/src/main/kotlin/utils/BuildUtils.kt index 64ae88f37a..854db1182d 100644 --- a/build-system/conventions/src/main/kotlin/utils/BuildUtils.kt +++ b/build-system/conventions/src/main/kotlin/utils/BuildUtils.kt @@ -104,9 +104,3 @@ fun Project.isAndroidLib(): Boolean = */ fun Project.isComposeLib(): Boolean = extensions.findByType()?.buildFeatures?.compose ?: false - -/** - * Возвращает true, если текущий проект - интеграционный модуль песочницы, иначе false - */ -fun Project.isSandboxIntegrationModule(): Boolean = - plugins.hasPlugin("convention.integration-view") || plugins.hasPlugin("convention.integration-compose") diff --git a/build-system/conventions/src/main/kotlin/utils/DocsProjectUtils.kt b/build-system/conventions/src/main/kotlin/utils/DocsProjectUtils.kt index f9b5ede08f..1336107d0e 100644 --- a/build-system/conventions/src/main/kotlin/utils/DocsProjectUtils.kt +++ b/build-system/conventions/src/main/kotlin/utils/DocsProjectUtils.kt @@ -197,17 +197,22 @@ private fun Project.getArtifactDocsBaseUrl(artifactId: String, version: String, private fun String.replaceKotlinSnippets(snippetsDir: File): String { return this.replace("//\\s*@sample:\\s*(.+)".toRegex()) { m -> val path = m.groupValues[1].trim() - snippetsDir.resolve(path).readText().trim() + snippetsDir.resolveSnippet(path, "kotlin").readText().trim() } } private fun String.replaceXmlSnippets(snippetsDir: File): String { return this.replace("".toRegex()) { m -> val path = m.groupValues[1].trim() - snippetsDir.resolve(path).readText().trim() + snippetsDir.resolveSnippet(path, "xml").readText().trim() } } +private fun File.resolveSnippet(path: String, platformDirectory: String): File { + val direct = resolve(path) + return if (direct.isFile) direct else resolve("$platformDirectory/$path") +} + private fun String.replaceScreenshots(templateDir: File, needScreenshots: Boolean, logger: Logger): String { val SCREENSHOT_REGEX = "".toRegex() diff --git a/build-system/conventions/src/main/resources/ComposeBindingDeclarationEmptyKt.txt b/build-system/conventions/src/main/resources/ComposeBindingDeclarationEmptyKt.txt deleted file mode 100644 index a09ddb2f9c..0000000000 --- a/build-system/conventions/src/main/resources/ComposeBindingDeclarationEmptyKt.txt +++ /dev/null @@ -1 +0,0 @@ -override val bindings: Set> = emptySet() diff --git a/build-system/conventions/src/test/kotlin/tasks/docs/ValidateCoreDocumentationTaskTest.kt b/build-system/conventions/src/test/kotlin/tasks/docs/ValidateCoreDocumentationTaskTest.kt new file mode 100644 index 0000000000..803678e93b --- /dev/null +++ b/build-system/conventions/src/test/kotlin/tasks/docs/ValidateCoreDocumentationTaskTest.kt @@ -0,0 +1,46 @@ +package tasks.docs + +import org.gradle.api.GradleException +import org.gradle.testfixtures.ProjectBuilder +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class ValidateCoreDocumentationTaskTest { + + @get:Rule + val temporaryFolder = TemporaryFolder() + + @Test + fun `structure lists every markdown template`() { + val task = configuredTask( + """{"navigation":[{"title":"Page","path":"components/Page.md"}]}""", + ) + task.docsDirectory.get().asFile.resolve("components/Page.md").apply { + parentFile.mkdirs() + writeText("# Page") + } + + task.validate() + } + + @Test + fun `unlisted markdown fails validation`() { + val task = configuredTask("""{"navigation":[]}""") + task.docsDirectory.get().asFile.resolve("Draft.md").writeText("# Draft") + + val error = runCatching(task::validate).exceptionOrNull() + + assertTrue(error is GradleException) + assertTrue(error?.message.orEmpty().contains("Draft.md")) + } + + private fun configuredTask(structure: String): ValidateCoreDocumentationTask { + val project = ProjectBuilder.builder().withProjectDir(temporaryFolder.newFolder()).build() + return project.tasks.create("validateDocs", ValidateCoreDocumentationTask::class.java).apply { + structureFile.set(temporaryFolder.newFile().apply { writeText(structure) }) + docsDirectory.set(temporaryFolder.newFolder()) + } + } +} diff --git a/build-system/conventions/src/test/kotlin/utils/DocsProjectUtilsTest.kt b/build-system/conventions/src/test/kotlin/utils/DocsProjectUtilsTest.kt new file mode 100644 index 0000000000..63ec496e40 --- /dev/null +++ b/build-system/conventions/src/test/kotlin/utils/DocsProjectUtilsTest.kt @@ -0,0 +1,31 @@ +package utils + +import org.gradle.testfixtures.ProjectBuilder +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class DocsProjectUtilsTest { + + @get:Rule + val temporaryFolder = TemporaryFolder() + + @Test + fun `merge plus prefixed docs appends override to core page`() { + val project = ProjectBuilder.builder().withProjectDir(temporaryFolder.root).build() + val docs = temporaryFolder.newFolder("docs") + docs.resolve("components").mkdirs() + val core = docs.resolve("components/Page.md").apply { writeText("core") } + val append = docs.resolve("components/+Page.md").apply { writeText("user") } + + project.mergePlusPrefixedDocs(docs) + + assertEquals( + "core\n\n\n\nuser", + core.readText(), + ) + assertFalse(append.exists()) + } +} diff --git a/build-system/docs-template/compose-template/structure.json b/build-system/docs-template/compose-template/structure.json new file mode 100644 index 0000000000..a33eea4d84 --- /dev/null +++ b/build-system/docs-template/compose-template/structure.json @@ -0,0 +1,99 @@ +{ + "schemaVersion": "1.0", + "navigation": [ + {"title": "Быстрый старт", "path": "quick_start.md"}, + { + "title": "Тема", + "items": [ + {"title": "Colors", "path": "theme/Colors.md"}, + {"title": "Shapes", "path": "theme/Shapes.md"}, + {"title": "Styles", "path": "theme/Styles.md"}, + {"title": "SubThemes", "path": "theme/SubThemes.md"}, + {"title": "Typography", "path": "theme/Typography.md"} + ] + }, + { + "title": "Визуальные эффекты", + "items": [ + {"title": "Blur", "path": "graphics/BlurUsage.md"}, + {"title": "Cutout", "path": "graphics/CutoutUsage.md"}, + {"title": "Indication", "path": "graphics/IndicationUsage.md"}, + {"title": "Motion API", "path": "graphics/MotionAPI.md"} + ] + }, + { + "title": "Компоненты", + "items": [ + {"title": "Accordion", "path": "components/AccordionUsage.md"}, + {"title": "AiAnswer", "path": "components/AiAnswerUsage.md"}, + {"title": "AiHeader", "path": "components/AiHeaderUsage.md"}, + {"title": "AiInput", "path": "components/AiInputUsage.md"}, + {"title": "AiUserMessage", "path": "components/AiUserMessageUsage.md"}, + {"title": "Autocomplete", "path": "components/AutocompleteUsage.md"}, + {"title": "AvatarGroup", "path": "components/AvatarGroupUsage.md"}, + {"title": "Avatar", "path": "components/AvatarUsage.md"}, + {"title": "Badge", "path": "components/BadgeUsage.md"}, + {"title": "BasicButton", "path": "components/BasicButtonUsage.md"}, + {"title": "BottomSheet", "path": "components/BottomSheetUsage.md"}, + {"title": "ButtonGroup", "path": "components/ButtonGroupUsage.md"}, + {"title": "Card", "path": "components/CardUsage.md"}, + {"title": "Carousel", "path": "components/CarouselUsage.md"}, + {"title": "Cell", "path": "components/CellUsage.md"}, + {"title": "CheckBoxGroup", "path": "components/CheckBoxGroupUsage.md"}, + {"title": "CheckBox", "path": "components/CheckBoxUsage.md"}, + {"title": "ChipGroup", "path": "components/ChipGroupUsage.md"}, + {"title": "Chip", "path": "components/ChipUsage.md"}, + {"title": "CircularProgressBar", "path": "components/CircularProgressBarUsage.md"}, + {"title": "CodeField", "path": "components/CodeFieldUsage.md"}, + {"title": "CodeInput", "path": "components/CodeInputUsage.md"}, + {"title": "CollapsingNavigationBar", "path": "components/CollapsingNavigationBarUsage.md"}, + {"title": "ComboBox", "path": "components/ComboBoxUsage.md"}, + {"title": "Counter", "path": "components/CounterUsage.md"}, + {"title": "Divider", "path": "components/DividerUsage.md"}, + {"title": "Drawer", "path": "components/DrawerUsage.md"}, + {"title": "DropZone", "path": "components/DropZoneUsage.md"}, + {"title": "DropdownMenu", "path": "components/DropdownMenuUsage.md"}, + {"title": "Editable", "path": "components/EditableUsage.md"}, + {"title": "File", "path": "components/FileUsage.md"}, + {"title": "FormItem", "path": "components/FormItemUsage.md"}, + {"title": "IconButton", "path": "components/IconButtonUsage.md"}, + {"title": "Icon", "path": "components/IconUsage.md"}, + {"title": "Image", "path": "components/ImageUsage.md"}, + {"title": "Indicator", "path": "components/IndicatorUsage.md"}, + {"title": "List", "path": "components/ListUsage.md"}, + {"title": "Loader", "path": "components/LoaderUsage.md"}, + {"title": "Mask", "path": "components/MaskUsage.md"}, + {"title": "Modal", "path": "components/ModalUsage.md"}, + {"title": "NavigationBar", "path": "components/NavigationBarUsage.md"}, + {"title": "NavigationDrawer", "path": "components/NavigationDrawerUsage.md"}, + {"title": "Note", "path": "components/NoteUsage.md"}, + {"title": "NotificationContent", "path": "components/NotificationContentUsage.md"}, + {"title": "Notification", "path": "components/NotificationUsage.md"}, + {"title": "Overlay", "path": "components/OverlayUsage.md"}, + {"title": "PaginationDots", "path": "components/PaginationDotsUsage.md"}, + {"title": "Popover", "path": "components/PopoverUsage.md"}, + {"title": "ProgressBar", "path": "components/ProgressBarUsage.md"}, + {"title": "RadioBoxGroup", "path": "components/RadioBoxGroupUsage.md"}, + {"title": "RadioBox", "path": "components/RadioBoxUsage.md"}, + {"title": "RectSkeleton", "path": "components/RectSkeletonUsage.md"}, + {"title": "ScrollBar", "path": "components/ScrollBarUsage.md"}, + {"title": "SegmentItem", "path": "components/SegmentItemUsage.md"}, + {"title": "Segment", "path": "components/SegmentUsage.md"}, + {"title": "Select", "path": "components/SelectUsage.md"}, + {"title": "Slider", "path": "components/SliderUsage.md"}, + {"title": "Spinner", "path": "components/SpinnerUsage.md"}, + {"title": "Splitter", "path": "components/SplittterUsage.md"}, + {"title": "Switch", "path": "components/SwitchUsage.md"}, + {"title": "TabBar", "path": "components/TabBarUsage.md"}, + {"title": "Tabs", "path": "components/TabsUsage.md"}, + {"title": "TextField", "path": "components/TextFieldUsage.md"}, + {"title": "TextSkeleton", "path": "components/TextSkeletonUsage.md"}, + {"title": "Text", "path": "components/TextUsage.md"}, + {"title": "Toast", "path": "components/ToastUsage.md"}, + {"title": "ToolBar", "path": "components/ToolBarUsage.md"}, + {"title": "Tooltip", "path": "components/TooltipUsage.md"}, + {"title": "Wheel", "path": "components/WheelUsage.md"} + ] + } + ] +} diff --git a/build-system/docs-template/xml-template/structure.json b/build-system/docs-template/xml-template/structure.json new file mode 100644 index 0000000000..79ccbabd60 --- /dev/null +++ b/build-system/docs-template/xml-template/structure.json @@ -0,0 +1,78 @@ +{ + "schemaVersion": "1.0", + "navigation": [ + {"title": "Быстрый старт", "path": "quick_start.md"}, + { + "title": "Тема", + "items": [ + {"title": "Colors", "path": "theme/Colors.md"}, + {"title": "ShapeAppearance", "path": "theme/ShapeAppearance.md"}, + {"title": "SubThemes", "path": "theme/SubThemes.md"}, + {"title": "Typography", "path": "theme/Typography.md"} + ] + }, + { + "title": "Компоненты", + "items": [ + {"title": "Accordion", "path": "components/AccordionUsage.md"}, + {"title": "Autocomplete", "path": "components/AutocompleteUsage.md"}, + {"title": "AvatarGroup", "path": "components/AvatarGroupUsage.md"}, + {"title": "Avatar", "path": "components/AvatarUsage.md"}, + {"title": "Badge", "path": "components/BadgeUsage.md"}, + {"title": "BasicButton", "path": "components/Button/BasicButtonUsage.md"}, + {"title": "IconButton", "path": "components/Button/IconButtonUsage.md"}, + {"title": "LinkButton", "path": "components/Button/LinkButtonUsage.md"}, + {"title": "ButtonGroup", "path": "components/ButtonGroupUsage.md"}, + {"title": "Card", "path": "components/CardUsage.md"}, + {"title": "Carousel", "path": "components/CarouselUsage.md"}, + {"title": "Cell", "path": "components/CellUsage.md"}, + {"title": "CheckBoxGroup", "path": "components/CheckBoxGroupUsage.md"}, + {"title": "CheckBox", "path": "components/CheckBoxUsage.md"}, + {"title": "ChipGroup", "path": "components/ChipGroupUsage.md"}, + {"title": "Chip", "path": "components/ChipUsage.md"}, + {"title": "CircularProgressBar", "path": "components/CircularProgressBarUsage.md"}, + {"title": "CodeField", "path": "components/CodeFieldUsage.md"}, + {"title": "CodeInput", "path": "components/CodeInputUsage.md"}, + {"title": "Counter", "path": "components/CounterUsage.md"}, + {"title": "Divider", "path": "components/DividerUsage.md"}, + {"title": "Drawer", "path": "components/DrawerUsage.md"}, + {"title": "DropDownMenu", "path": "components/DropDownMenuUsage.md"}, + {"title": "Editable", "path": "components/EditableUsage.md"}, + {"title": "File", "path": "components/FileUssage.md"}, + {"title": "Flow", "path": "components/FlowUsage.md"}, + {"title": "Indicator", "path": "components/IndicatorUsage.md"}, + {"title": "ListItem", "path": "components/ListItemUsage.md"}, + {"title": "List", "path": "components/ListUsage.md"}, + {"title": "Loader", "path": "components/LoaderUsage.md"}, + {"title": "Mask", "path": "components/MaskUsage.md"}, + {"title": "Modal", "path": "components/ModalUsage.md"}, + {"title": "NavigationBar", "path": "components/NavigationBarUsage.md"}, + {"title": "NavigationDrawer", "path": "components/NavigationDrawerUsage.md"}, + {"title": "Note", "path": "components/NoteUsage.md"}, + {"title": "NotificationContent", "path": "components/NotificationContentUsage.md"}, + {"title": "Notification", "path": "components/NotificationUsage.md"}, + {"title": "Overlay", "path": "components/OverlayUsage.md"}, + {"title": "PaginationDots", "path": "components/PaginationDotsUsage.md"}, + {"title": "Popover", "path": "components/PopoverUsage.md"}, + {"title": "Progress", "path": "components/ProgressUsage.md"}, + {"title": "RadioBoxGroup", "path": "components/RadioBoxGroupUsage.md"}, + {"title": "RadioBox", "path": "components/RadioBoxUsage.md"}, + {"title": "RectSkeleton", "path": "components/RectSkeletonUsage.md"}, + {"title": "ScrollBar", "path": "components/ScrollBarUsage.md"}, + {"title": "Segment", "path": "components/SegmentUsage.md"}, + {"title": "Select", "path": "components/SelectUsage.md"}, + {"title": "Slider", "path": "components/SliderUsage.md"}, + {"title": "Spinner", "path": "components/SpinnerUsage.md"}, + {"title": "Switch", "path": "components/SwitchUsage.md"}, + {"title": "Tabs", "path": "components/TabsUsage.md"}, + {"title": "TextField", "path": "components/TextFieldUsage.md"}, + {"title": "TextSkeleton", "path": "components/TextSkeletonUsage.md"}, + {"title": "Toast", "path": "components/ToastUsage.md"}, + {"title": "ToolBar", "path": "components/ToolBarUsage.md"}, + {"title": "Tooltip", "path": "components/TooltipUsage.md"}, + {"title": "Wheel", "path": "components/WheelUsage.md"} + ] + }, + {"title": "Фокус", "path": "focus.md"} + ] +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9c7cd99007..d383fcc541 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -52,6 +52,8 @@ koilVersion-compose = "2.2.0" sdds-uikit = "0.47.0" sdds-uikit-compose = "0.49.0" sdds-theme-builder = "0.46.0" +sdds-dsbuilder = "0.46.0" + sdds-haze = "0.4.0" uikit-input-masks-version = "7.2.6" @@ -168,7 +170,7 @@ jgp = { id = "java-gradle-plugin" } android-cache-fix = { id = "org.gradle.android.cache-fix", version.ref= "plugin-androidCacheFix" } gradleNexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "plugin-gradleNexusPublish" } gradlePluginPublish = { id = "com.gradle.plugin-publish", version.ref="plugin-gradlePluginPublish"} -themebuilder = { id = "io.github.salute-developers.theme-builder-plugin", version.ref="sdds-theme-builder" } +dsbuilder = { id = "io.github.salute-developers.design-system-builder", version.ref="sdds-dsbuilder" } binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref="plugin-binary-compatibility-validator" } roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "test-roborazzi" } dokka = { id = "org.jetbrains.dokka", version.ref = "plugin-dokka"} diff --git a/integration-core/sandbox-compose/build.gradle.kts b/integration-core/sandbox-compose/build.gradle.kts index 802af03225..1bd0357847 100644 --- a/integration-core/sandbox-compose/build.gradle.kts +++ b/integration-core/sandbox-compose/build.gradle.kts @@ -12,7 +12,7 @@ plugins { id("convention.cmp-lib") id("convention.maven-publish") id("convention.auto-bump") - id(libs.plugins.themebuilder.get().pluginId) + id(libs.plugins.dsbuilder.get().pluginId) } group = "integration-core" @@ -21,16 +21,18 @@ android { namespace = "com.sdds.compose.sandbox" } -themeBuilder { - themeSource(name = themeName, version = themeVersion, alias = themeAlias) - componentSource(name = componentsName, version = componentsVersion, alias = themeAlias) - compose { - multiplatform(true) +dsBuilder { + autoGenerate.set(false) + compose(multiplatform = true) + packageName.set("com.sdds.compose.sandbox") + outputLocation.set(SRC) + theme { + source(name = themeName, version = themeVersion, alias = themeAlias) + mode.set(THEME) + } + components { + source(name = componentsName, version = componentsVersion, alias = themeAlias) } - ktPackage(ktPackage = "com.sdds.compose.sandbox") - mode(THEME) - autoGenerate(false) - outputLocation(SRC) } kotlin { diff --git a/integration-core/sandbox-view/build.gradle.kts b/integration-core/sandbox-view/build.gradle.kts index 64726bea69..174463560a 100644 --- a/integration-core/sandbox-view/build.gradle.kts +++ b/integration-core/sandbox-view/build.gradle.kts @@ -1,18 +1,9 @@ -import com.sdds.plugin.themebuilder.OutputLocation.SRC -import com.sdds.plugin.themebuilder.ThemeBuilderMode.THEME -import utils.componentsName -import utils.componentsVersion -import utils.themeAlias -import utils.themeName -import utils.themeVersion - @Suppress("DSL_SCOPE_VIOLATION") plugins { id("convention.android-lib") id("convention.maven-publish") id("convention.compose") id("convention.auto-bump") - id(libs.plugins.themebuilder.get().pluginId) } group = "integration-core" @@ -21,16 +12,6 @@ android { namespace = "com.sdds.view.sandbox" } -themeBuilder { - themeSource(name = themeName, version = themeVersion, alias = themeAlias) - componentSource(name = componentsName, version = componentsVersion, alias = themeAlias) - compose() - ktPackage(ktPackage = "com.sdds.view.sandbox") - mode(THEME) - autoGenerate(false) - outputLocation(SRC) -} - dependencies { implementation(project(":sandbox-core")) implementation(project(":sandbox-compose")) diff --git a/openspec/changes/archive/2026-07-28-aggregate-user-documentation-layer/.openspec.yaml b/openspec/changes/archive/2026-07-28-aggregate-user-documentation-layer/.openspec.yaml new file mode 100644 index 0000000000..e8209ffaac --- /dev/null +++ b/openspec/changes/archive/2026-07-28-aggregate-user-documentation-layer/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-28 diff --git a/openspec/changes/archive/2026-07-28-aggregate-user-documentation-layer/design.md b/openspec/changes/archive/2026-07-28-aggregate-user-documentation-layer/design.md new file mode 100644 index 0000000000..d0415abfe4 --- /dev/null +++ b/openspec/changes/archive/2026-07-28-aggregate-user-documentation-layer/design.md @@ -0,0 +1,152 @@ +## Context + +После change `package-core-documentation-templates` Android plugin получает версионированный Core docs JAR, насыщает публичный Core markdown и формирует `.sdds/temp/docs`. User documentation уже существует в `tokens/*/docs/override-docs`, но legacy Docusaurus воспринимает её как файловый overlay: обычный файл заменяет Core page, а `+Name.md` дописывается к ней функцией `mergePlusPrefixedDocs`. + +ADR-0003 требует сохранять Core и user content раздельно, чтобы DS Builder CLI построил итоговую navigation и `contentRefs` с `source=core|user`. Поэтому Android не должен повторять merge Docusaurus, но обязан разрешить физические legacy sources в логические user paths, насытить их теми же platform data и передать обе structures. + +Текущие реальные user markdown ограничены `plasma.homeds.compose` (три append и две standalone pages) и `sdds-sbcom-compose` (одна append page). Остальные `override-docs` содержат screenshots и не требуют фиктивной user structure. + +## Goals / Non-Goals + +**Goals:** + +- ввести явный user source contract в `override-docs/structure.json`; +- сохранить текущий Docusaurus overlay и `+*.md` append без изменения результата; +- насыщать Core и user markdown одним processor, но записывать в разные namespaces; +- передавать CLI независимые `structure-core.json` и `structure-user.json`; +- мигрировать существующие user overrides на декларативную структуру; +- валидировать неоднозначные logical/physical paths и merge modes. + +**Non-Goals:** + +- merge Core и user navigation/content; +- формирование `docs.json`, `manifest.json` и `contentRefs`; +- поддержка `prepend` в legacy Docusaurus; +- удаление `+` convention; +- создание user docs для token modules, где сейчас есть только screenshots; +- Docusaurus build, npm, S3 upload и publication; +- изменение Core docs JAR или публикационного Gradle variant. + +## Decisions + +### 1. User source root остаётся `override-docs` + +Структура располагается рядом с существующей директорией `docs`: + +```text +override-docs/ +├── structure.json +├── docs/ +│ ├── components/+EditableUsage.md +│ └── components/FloatingButtonBar.md +└── static/screenshots-docusaurus/ +``` + +Это сохраняет единственный авторский источник и повторяет layout Core template, где `structure.json` расположен рядом с `docs/`. Альтернатива с новой директорией `user-docs` отклонена: она потребовала бы дублировать или перемещать legacy Docusaurus inputs. + +`DocumentationCapability` получает configurable user root с convention на `project.layout.projectDirectory.dir("override-docs")`. Directory является optional Gradle input; отсутствие structure означает отсутствие user layer. + +### 2. Structure содержит logical path, filesystem сохраняет legacy marker + +Для append к Core page: + +```text +structure path: components/EditableUsage.md +merge: append +physical source: docs/components/+EditableUsage.md +output: content/user/components/EditableUsage.md +``` + +Префикс `+` не попадает ни в structure output, ни в content path. Он остаётся адаптером только для `mergePlusPrefixedDocs`. + +Для standalone page physical и logical paths совпадают. Для replace существующей Core page используется обычный файл и явный `merge: replace`, что соответствует текущему overlay Docusaurus. + +Альтернатива хранить `+` в structure path отклонена: тогда CLI не сможет сопоставить user page с Core page по path. Альтернатива немедленно удалить `+` отклонена: legacy Docusaurus потеряет append semantics. + +### 3. Пересечения с Core требуют явного merge + +Aggregator строит allowlist Core logical paths и валидирует user navigation: + +- новый path → обычный markdown, поле `merge` отсутствует; +- Core path + `append` → только `+Name.md`; +- Core path + `replace` → только обычный `Name.md`; +- Core path без `merge` → ошибка; +- new path с `+Name.md` → ошибка; +- `prepend` → ошибка до миграции legacy Docusaurus. + +Явность предотвращает расхождение нового bundle и Docusaurus. Хотя ADR называет append значением по умолчанию, переходный Android contract требует explicit merge для пересечений, потому что физическое имя файла определяет legacy поведение. + +### 4. Enrichment разделяется на parsing и materialization + +Существующий Core-specific container обобщается до документационного слоя: + +- structure JSON; +- logical path → source markdown; +- source label (`core`/`user`); +- output namespace. + +Один processor заменяет Kotlin/XML samples и Compose style API и сохраняет screenshot directives. Это исключает расхождение поведения Core и user pages. Ошибки unresolved samples содержат source layer, logical path и reference. + +### 5. Platform output всегда сохраняет происхождение content + +Новый layout: + +```text +.sdds/temp/docs/ +├── structure-core.json +├── structure-user.json # только если user layer существует +├── content/ +│ ├── core/** # только если Core structure существует +│ └── user/** # только если user structure существует +├── assets/ +│ ├── examples/{kotlin,xml}/** +│ └── screenshots/** +└── meta/** +``` + +Android не объединяет одинаковые logical paths. Раздельные namespaces позволяют CLI сформировать `contentRefs` без копирования или повторного platform enrichment. + +Это breaking change относительно недавно введённого `content/**` Core output, но потребителей финального Android layout ещё нет; корректировка сейчас дешевле, чем поддержка неоднозначного промежуточного формата. + +### 6. User structure валидируется рядом с platform aggregation + +Основная runtime validation выполняется в `DocumentationAggregateTask`, потому что ей доступны обе structures и можно определить collision с Core. Для существующих token sources добавляются focused tests, проверяющие: + +- все user navigation paths разрешаются; +- `+` соответствует `append`; +- standalone pages не используют `+`; +- unlisted markdown не публикуется. + +Legacy Docusaurus task не начинает интерпретировать structure в этом change. Его regression-проверка подтверждает, что `mergePlusPrefixedDocs` продолжает выдавать прежний объединённый markdown. + +### 7. Относительные ссылки должны быть bundle-safe + +User markdown SHALL ссылаться на Core/user pages логическими относительными ссылками внутри будущего content tree, а не путями к `build-system/docs-template`. Существующие repository-relative ссылки в `FloatingButtonBar.md` мигрируют на ссылки, которые DS Builder CLI сможет разрешить после merge. + +Полная link validation остаётся ответственностью CLI, но Android tests фиксируют отсутствие прямых ссылок на repository template paths в публикуемом user content. + +## Risks / Trade-offs + +- [Logical path отличается от physical `+` source] → централизовать resolver и покрыть append/replace/new page table-driven tests. +- [Core и user output layout breaking для ранних потребителей] → изменить до появления CLI consumer и явно проверить отсутствие файлов непосредственно под `content`. +- [Docusaurus и structure могут разойтись] → валидировать текущие overrides и сохранять один physical markdown source. +- [ADR допускает prepend, а legacy Docusaurus нет] → fail fast с диагностикой; добавить режим после миграции legacy generator. +- [Обычный colliding file без merge может случайно заменить Core в Docusaurus] → считать это ошибкой Android validation и требовать explicit `replace`. +- [User component отсутствует в components info] → выдавать ошибку с user logical path при `@style-api`, как для Core enrichment. +- [Общие screenshot/example namespaces могут содержать collisions] → сохранять текущие стабильные IDs и fail fast при разных файлах на одном output path. + +## Migration Plan + +1. Перевести Core output в `content/core` и обновить tests. +2. Добавить optional user root/structure inputs и logical source resolver. +3. Добавить `structure.json` к `plasma.homeds.compose` и `sdds-sbcom-compose`, не переименовывая `+*.md`. +4. Насыщать user pages в `content/user` и сохранить `structure-user.json`. +5. Исправить repository-relative links в standalone user pages. +6. Проверить plugin unit tests, обе существующие user structures, Compose aggregation с user layer, View aggregation без него и legacy Docusaurus merge. + +Rollback возвращает Core output в `content/**` и отключает optional user inputs. Файлы `override-docs/structure.json` не влияют на legacy Docusaurus и могут оставаться как неиспользуемые декларативные данные. + +## Open Questions + +- Нужно ли следующим change перевести legacy Docusaurus на чтение `structure.json`, чтобы разрешить удаление `+` и поддержку `prepend`? +- Должен ли CLI валидировать совпадение screenshot keys с filenames до S3 upload или это останется ответственностью documentation service? diff --git a/openspec/changes/archive/2026-07-28-aggregate-user-documentation-layer/proposal.md b/openspec/changes/archive/2026-07-28-aggregate-user-documentation-layer/proposal.md new file mode 100644 index 0000000000..47c0b503dd --- /dev/null +++ b/openspec/changes/archive/2026-07-28-aggregate-user-documentation-layer/proposal.md @@ -0,0 +1,32 @@ +## Why + +Android documentation aggregation currently emits only enriched Core pages, while ADR-0003 requires the final documentation bundle to preserve both Core and design-system-specific user layers for a later merge by DS Builder CLI. Existing user documentation already lives in `tokens/*/docs/override-docs`, but it has no declarative structure and relies on the legacy Docusaurus `+*.md` append convention. + +## What Changes + +- Introduce an optional user documentation source rooted at `override-docs`, with an explicit `structure.json` and public markdown under `docs/**/*.md`. +- Enrich public user markdown with the same Kotlin/XML samples and Compose style API as Core markdown, while preserving screenshot keys and copying screenshot assets. +- **BREAKING**: separate platform output into `.sdds/temp/docs/content/core/**` and `.sdds/temp/docs/content/user/**` instead of writing Core pages directly under `content/**`. +- Emit the two source structures independently as `structure-core.json` and optional `structure-user.json`; DS Builder CLI remains responsible for navigation merge and final `contentRefs`. +- Preserve legacy Docusaurus behavior: `+Name.md` remains the physical source for an `append` user page, while `structure.json` uses the logical path `Name.md` without the prefix. +- Add declarative user structures for the existing `plasma.homeds.compose` and `sdds-sbcom-compose` overrides and validate that every public path resolves to the correct ordinary or `+`-prefixed markdown source. +- Keep Docusaurus generation, Core/user merge, `docs.json`/`manifest.json`, S3 upload and publication outside the Android aggregation task. + +## Capabilities + +### New Capabilities + +- `user-documentation-layer`: Defines the Android user documentation source layout, explicit navigation, legacy `+*.md` compatibility and independently enriched output. + +### Modified Capabilities + +- `android-documentation-aggregation`: Separates Core and user content namespaces and enriches both layers without merging them. + +## Impact + +- `sdds-core:plugin_theme_builder`: documentation DSL/task inputs, enrichment routing, output layout and unit tests change. +- `tokens/*/docs`: existing user markdown becomes explicitly described by `override-docs/structure.json`; current Docusaurus source files remain compatible. +- `build-system`: shared validation may be extended for the logical-path-to-physical-source mapping, without changing the legacy Docusaurus merge contract. +- DS Builder CLI must consume `structure-core.json`, optional `structure-user.json`, `content/core/**` and `content/user/**`; implementing that consumer remains outside this repository change. +- The change affects documentation generation and Gradle task contracts, but does not change UIKit public API or design tokens. +- Validation covers focused plugin tests, existing override documentation, legacy Docusaurus generation, and Compose/View `documentationAggregate` outputs. diff --git a/openspec/changes/archive/2026-07-28-aggregate-user-documentation-layer/specs/android-documentation-aggregation/spec.md b/openspec/changes/archive/2026-07-28-aggregate-user-documentation-layer/specs/android-documentation-aggregation/spec.md new file mode 100644 index 0000000000..ee0b11adca --- /dev/null +++ b/openspec/changes/archive/2026-07-28-aggregate-user-documentation-layer/specs/android-documentation-aggregation/spec.md @@ -0,0 +1,90 @@ +## MODIFIED Requirements + +### Requirement: Агрегированный результат пригоден для DS Builder CLI +Documentation capability SHALL производить раздельный платформенно насыщенный Core и user результат с Gradle-declared inputs и outputs, который DS Builder CLI может объединить без знания исходных source sets, Docusaurus и задач Android-проекта. + +#### Scenario: Задача агрегации завершена +- **WHEN** выполняется lifecycle-задача агрегации документации с Core и user structures +- **THEN** `.sdds/temp/docs` SHALL содержать Core markdown в `content/core`, user markdown в `content/user`, `structure-core.json`, `structure-user.json`, snippets в `assets/examples/kotlin` и `assets/examples/xml`, screenshots в `assets/screenshots`, metadata в `meta/samples.json` и доступные platform info files в `meta` + +#### Scenario: Задача агрегации завершена без user layer +- **WHEN** user structure отсутствует +- **THEN** Core output и общие artifacts SHALL быть созданы, а `structure-user.json` и `content/user` SHALL отсутствовать + +#### Scenario: Sample path разрешается от documentation root +- **WHEN** aggregator записывает Kotlin или XML sample metadata +- **THEN** `snippetPath` SHALL быть относительным к `.sdds/temp/docs` и начинаться с `assets/examples/kotlin/` или `assets/examples/xml/` + +#### Scenario: Входной файл отсутствует +- **WHEN** обязательный для выбранной платформы info-файл отсутствует +- **THEN** задача MUST завершиться ошибкой с точным путём отсутствующего файла + +#### Scenario: Core template отсутствует +- **WHEN** Core documentation artifact не содержит `structure.json` и markdown template +- **THEN** snippets, user layer и info-артефакты SHALL агрегироваться без создания фиктивной Core structure + +### Requirement: Core markdown насыщается платформенными examples +Documentation capability SHALL обрабатывать существующие Kotlin/XML sample directives в Core markdown и записывать результат в `.sdds/temp/docs/content/core`, сохраняя относительные логические пути страниц. + +#### Scenario: Kotlin sample существует +- **WHEN** Core markdown содержит Kotlin `@sample` directive, разрешимый по Core или локальным snippets +- **THEN** directive SHALL быть заменён содержимым соответствующего Kotlin example + +#### Scenario: XML sample существует +- **WHEN** Core markdown содержит XML `@sample` directive, разрешимый по Core или локальным snippets +- **THEN** directive SHALL быть заменён содержимым соответствующего XML example + +#### Scenario: Локальный example переопределяет Core example +- **WHEN** Core и локальный example разрешаются по одному пути +- **THEN** при насыщении SHALL использоваться локальный example + +#### Scenario: Обязательный sample не разрешается +- **WHEN** Core markdown содержит `@sample` directive, для которого отсутствует example +- **THEN** задача MUST завершиться ошибкой с путём markdown-файла и значением directive + +### Requirement: Core structure сопровождает насыщенный content +Documentation capability SHALL сохранять `structure.json` из Core documentation artifact как `structure-core.json` и SHALL сохранять Core content отдельно от user content. + +#### Scenario: Core structure валидно ссылается на templates +- **WHEN** все пути страниц из Core `structure.json` существуют в Core template +- **THEN** aggregator SHALL скопировать structure и создать соответствующие насыщенные файлы в `content/core` + +#### Scenario: Structure ссылается на отсутствующий markdown +- **WHEN** публичный path из Core `structure.json` отсутствует в Core template +- **THEN** задача MUST завершиться ошибкой с отсутствующим относительным путём + +#### Scenario: Template отсутствует в structure +- **WHEN** markdown-файл Core template не указан в Core `structure.json` +- **THEN** файл SHALL считаться непубличным и SHALL NOT попадать в `content/core` + +## ADDED Requirements + +### Requirement: User markdown насыщается независимо от Core +Documentation capability SHALL применять platform enrichment к публичному user markdown и SHALL сохранять его в `.sdds/temp/docs/content/user` без предварительного merge с Core content. + +#### Scenario: User sample существует +- **WHEN** публичный user markdown содержит разрешимый Kotlin или XML `@sample` directive +- **THEN** directive SHALL быть заменён соответствующим локальным example + +#### Scenario: User style API существует +- **WHEN** публичный Compose user markdown содержит `@style-api` и platform components info содержит соответствующий component +- **THEN** directive SHALL быть заменён таблицей параметров и примером выбора готового стиля + +#### Scenario: User screenshot key существует +- **WHEN** публичный user markdown содержит `@screenshot` +- **THEN** directive SHALL остаться неизменным, а доступный PNG SHALL находиться в общем `assets/screenshots` + +#### Scenario: Core и user имеют одинаковый logical path +- **WHEN** обе structures содержат один path +- **THEN** Android output SHALL содержать оба файла в разных namespaces и SHALL NOT объединять их содержимое + +### Requirement: User structure передаётся DS Builder CLI без merge +Documentation capability SHALL копировать валидный user structure как `.sdds/temp/docs/structure-user.json`, сохраняя navigation metadata и merge directives. + +#### Scenario: User structure валидно +- **WHEN** все публичные user paths разрешены и поддерживаемые merge modes валидны +- **THEN** aggregator SHALL записать `structure-user.json` без слияния с `structure-core.json` + +#### Scenario: Android aggregation получает merge metadata +- **WHEN** user page содержит `merge`, `hidden` или `subjects` +- **THEN** эти поля SHALL быть сохранены для последующей обработки DS Builder CLI diff --git a/openspec/changes/archive/2026-07-28-aggregate-user-documentation-layer/specs/user-documentation-layer/spec.md b/openspec/changes/archive/2026-07-28-aggregate-user-documentation-layer/specs/user-documentation-layer/spec.md new file mode 100644 index 0000000000..d3c1bfad0d --- /dev/null +++ b/openspec/changes/archive/2026-07-28-aggregate-user-documentation-layer/specs/user-documentation-layer/spec.md @@ -0,0 +1,66 @@ +## ADDED Requirements + +### Requirement: User documentation имеет явную структуру +Android documentation source SHALL принимать опциональный user layer из `override-docs/structure.json` и `override-docs/docs/**/*.md`, где structure явно перечисляет публичные логические пути страниц. + +#### Scenario: User layer отсутствует +- **WHEN** `override-docs/structure.json` отсутствует +- **THEN** Android aggregation SHALL завершиться без user structure и user content + +#### Scenario: User structure содержит самостоятельную страницу +- **WHEN** user structure ссылается на путь, отсутствующий в Core structure, и соответствующий markdown существует +- **THEN** страница SHALL считаться самостоятельной публичной user page + +#### Scenario: Markdown отсутствует в user structure +- **WHEN** user markdown не указан в `override-docs/structure.json` +- **THEN** файл SHALL считаться непубличным и SHALL NOT попадать в platform output + +#### Scenario: User structure ссылается на отсутствующий markdown +- **WHEN** публичный логический путь не разрешается в user markdown source +- **THEN** aggregation MUST завершиться ошибкой с логическим путём + +### Requirement: Legacy append сохраняет Docusaurus-совместимый source +User documentation SHALL поддерживать физический `+Name.md` как переходное представление логической страницы `Name.md` с `merge: append`. + +#### Scenario: Append дополняет Core page +- **WHEN** user structure содержит Core path с `merge: append` +- **THEN** source SHALL разрешаться из `+Name.md`, а user output SHALL сохранять логический путь `Name.md` без префикса + +#### Scenario: Legacy Docusaurus собирает append page +- **WHEN** выполняется существующая Docusaurus generation +- **THEN** `mergePlusPrefixedDocs` SHALL продолжать дописывать `+Name.md` к соответствующей Core page + +#### Scenario: Append source имеет неверное имя +- **WHEN** Core path объявлен с `merge: append`, но существует только обычный `Name.md` +- **THEN** validation MUST завершиться ошибкой и указать ожидаемый `+Name.md` + +#### Scenario: Самостоятельная page использует plus prefix +- **WHEN** логический path отсутствует в Core structure, но source назван `+Name.md` +- **THEN** validation MUST завершиться ошибкой, потому что plus prefix допустим только для append к Core page + +### Requirement: User merge contract однозначен для обоих pipelines +User structure SHALL явно отличать append и replace существующей Core page, а неподдерживаемый legacy merge mode SHALL отклоняться до насыщения. + +#### Scenario: User page заменяет Core page +- **WHEN** user structure содержит Core path с `merge: replace` и обычный `Name.md` +- **THEN** Android aggregation SHALL сохранить отдельный user content, а legacy Docusaurus SHALL продолжить использовать overlay replacement + +#### Scenario: Пересекающийся path не имеет merge +- **WHEN** user path существует в Core structure, но merge mode не задан +- **THEN** validation MUST завершиться ошибкой и потребовать явный `append` или `replace` + +#### Scenario: Prepend запрошен при включённой legacy совместимости +- **WHEN** user structure содержит `merge: prepend` +- **THEN** validation MUST завершиться понятной ошибкой, поскольку legacy Docusaurus не поддерживает prepend + +### Requirement: Существующие user overrides получают декларативную навигацию +Существующие user markdown в token docs modules SHALL быть описаны user structure без изменения их Docusaurus merge semantics. + +#### Scenario: Homeds overrides валидируются +- **WHEN** проверяется `plasma.homeds.compose/docs/override-docs` +- **THEN** все текущие append и standalone markdown SHALL быть перечислены в user structure с соответствующими логическими paths + +#### Scenario: Sbcom overrides валидируются +- **WHEN** проверяется `sdds-sbcom-compose/docs/override-docs` +- **THEN** текущий `+IndicationUsage.md` SHALL быть представлен логическим Core path с `merge: append` + diff --git a/openspec/changes/archive/2026-07-28-aggregate-user-documentation-layer/tasks.md b/openspec/changes/archive/2026-07-28-aggregate-user-documentation-layer/tasks.md new file mode 100644 index 0000000000..624c536cd7 --- /dev/null +++ b/openspec/changes/archive/2026-07-28-aggregate-user-documentation-layer/tasks.md @@ -0,0 +1,43 @@ +## 1. Token docs: декларативный user source + +- [x] 1.1 Добавить `tokens/plasma.homeds.compose/docs/override-docs/structure.json`, перечислив три существующих append override и две standalone pages с логическими путями без `+` +- [x] 1.2 Добавить `tokens/sdds-sbcom-compose/docs/override-docs/structure.json` для append override страницы Indication +- [x] 1.3 Исправить repository-relative ссылки в standalone user markdown на bundle-safe логические ссылки +- [x] 1.4 Проверить, что token modules без user markdown не получают фиктивный `structure.json` + +## 2. sdds-core: Gradle inputs и раздельный output + +- [x] 2.1 Расширить documentation capability/task optional user documentation root с convention на `override-docs` и обновить KDoc Gradle contract +- [x] 2.2 Перенести насыщенный Core markdown из `content/**` в `content/core/**`, сохранив `structure-core.json` +- [x] 2.3 Не создавать `structure-user.json` и `content/user` при отсутствии user structure +- [x] 2.4 Обновить plugin/task tests для нового breaking output layout и optional Gradle input + +## 3. sdds-core: user structure и legacy source resolver + +- [x] 3.1 Прочитать и провалидировать `override-docs/structure.json`, сохраняя navigation metadata без изменений +- [x] 3.2 Реализовать logical-to-physical resolver: standalone page → обычный markdown, Core append → `+Name.md`, Core replace → обычный markdown +- [x] 3.3 Отклонять Core collision без explicit merge, append без `+`, plus для standalone page, unsupported prepend и небезопасные paths +- [x] 3.4 Записывать только перечисленные user pages в `content/user/**` и user structure в `structure-user.json` +- [x] 3.5 Добавить unit-тесты standalone, append, replace, отсутствующего user layer, unlisted draft, missing source и всех invalid mapping scenarios + +## 4. sdds-core: единое platform enrichment + +- [x] 4.1 Обобщить Core enrichment processor для повторного использования Core и user layers без предварительного merge +- [x] 4.2 Насыщать user Kotlin/XML `@sample` directives локальными examples с диагностикой layer, logical path и reference +- [x] 4.3 Насыщать user Compose `@style-api`, сохраняя screenshot directives как ключи +- [x] 4.4 Сохранить общие `assets/examples`, `assets/screenshots` и `meta` outputs и добавить collision checks для несовпадающих assets +- [x] 4.5 Добавить regression-тест одинакового Core/user logical path с двумя отдельными output files + +## 5. Legacy Docusaurus compatibility + +- [x] 5.1 Добавить focused regression-тест, что `mergePlusPrefixedDocs` продолжает дописывать `+*.md` к Core page +- [x] 5.2 Проверить, что новые `override-docs/structure.json` не влияют на Docusaurus copy/transform и не публикуются как markdown +- [x] 5.3 Выполнить Docusaurus generation для `plasma.homeds.compose` и `sdds-sbcom-compose` и сравнить append/standalone semantics + +## 6. Интеграционная и статическая проверка + +- [x] 6.1 Выполнить focused unit-тесты `sdds-core:plugin_theme_builder` и затронутые `build-system:conventions` tests +- [x] 6.2 Выполнить `documentationAggregate` для Compose DS с user layer и проверить обе structures, `content/core`, `content/user`, examples, screenshots и meta +- [x] 6.3 Выполнить `documentationAggregate` для View DS без user layer и проверить только Core namespace +- [x] 6.4 Запустить релевантные detekt и Spotless задачи затронутых included builds из корня репозитория +- [x] 6.5 Выполнить strict OpenSpec validation и `git diff --check` diff --git a/openspec/changes/archive/2026-07-28-package-core-documentation-templates/.openspec.yaml b/openspec/changes/archive/2026-07-28-package-core-documentation-templates/.openspec.yaml new file mode 100644 index 0000000000..e8209ffaac --- /dev/null +++ b/openspec/changes/archive/2026-07-28-package-core-documentation-templates/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-28 diff --git a/openspec/changes/archive/2026-07-28-package-core-documentation-templates/design.md b/openspec/changes/archive/2026-07-28-package-core-documentation-templates/design.md new file mode 100644 index 0000000000..1306040e67 --- /dev/null +++ b/openspec/changes/archive/2026-07-28-package-core-documentation-templates/design.md @@ -0,0 +1,130 @@ +## Context + +ADR-0003 возлагает платформенное насыщение Core markdown на Android Docs Gradle Plugin и ожидает результат в `.sdds/temp/docs/content`. Сейчас `convention.core-fixtures` публикует classifier `docs` с извлечёнными snippets и metadata, а `DocumentationAggregateTask` распаковывает `META-INF/sdds-docs`, нормализует examples и добавляет platform info. Markdown-шаблоны при этом остаются в `build-system/docs-template/*-template` и используются только legacy Docusaurus. + +Изменение пересекает три included build: + +- `build-system` хранит исходные Compose/View markdown и собирает docs JAR; +- `integration-core` применяет convention к `uikit-compose-fixtures` и `uikit-fixtures`; +- `sdds-core:plugin_theme_builder` резолвит артефакт и создаёт платформенный результат. + +## Goals / Non-Goals + +**Goals:** + +- ввести `structure.json` для Compose и View на основе текущей навигации `sidebars.ts`; +- публиковать Core markdown, structure, snippets и metadata единым версионированным docs-артефактом fixtures; +- исключить из артефакта Docusaurus-инфраструктуру; +- насытить публичные Core markdown существующими Kotlin/XML examples; +- сформировать `.sdds/temp/docs/content` и передать Core structure DS Builder CLI; +- сохранить работу legacy snippets-only артефактов. + +**Non-Goals:** + +- генерация `docs.json` и `manifest.json`; +- merge Core и пользовательской документации; +- публикация пакета в documentation service; +- перенос или удаление Docusaurus pipeline; +- упаковка произвольных Docusaurus static assets; +- публикация fixtures/UIKit или введение BOM; +- включение генерируемого changelog. +- загрузка screenshots в S3 и формирование публичных screenshot URL. + +## Decisions + +### 1. `structure.json` хранится рядом с `sidebars.ts` + +Для каждой платформы добавляется собственный файл: + +- `build-system/docs-template/compose-template/structure.json`; +- `build-system/docs-template/xml-template/structure.json`. + +Он явно перечисляет все публичные markdown paths. Категории повторяют верхнеуровневый порядок `sidebars.ts`, а страницы внутри бывших `autogenerated` sections фиксируются явно и детерминированно. `CHANGELOG.md` не включается, поскольку создаётся отдельной Docusaurus-задачей и отсутствует в Core template. + +Альтернатива — генерировать structure из TypeScript — отклонена: это связывает DS Builder contract с Docusaurus и Node.js и сохраняет неявную autogenerated-навигацию. + +### 2. Docs JAR имеет allowlist для шаблонной части + +`convention.core-fixtures` получает соответствующий платформе template root, но включает из него только: + +```text +META-INF/sdds-docs/ +├── structure.json +└── docs/**/*.md +``` + +Извлечённые examples и metadata располагаются отдельно: + +```text +META-INF/sdds-docs/ +├── assets/examples/kotlin/... +├── assets/examples/xml/... +└── meta/samples.json +``` + +Allowlist выбран вместо набора excludes: новые Docusaurus-файлы не смогут случайно стать частью публичного артефакта. + +### 3. Один артефакт связывает templates и examples одной версией + +Дополнительный Maven artifact с classifier `docs` остаётся единицей публикации. Он собирается в fixtures-модулях, где уже находятся примеры соответствующей платформы. Это обеспечивает одинаковую версию structure, markdown и samples без BOM в рамках change. + +Gradle attribute `com.sdds.docs.variant` меняется с `snippets` на `templates` одновременно у producer в `convention.core-fixtures` и consumer в DS Builder Gradle Plugin. Совместимый `snippets` variant и отдельный второй JAR не публикуются: новое имя отражает основное назначение артефакта, включающего structure, markdown, examples и metadata. + +### 4. Aggregator обрабатывает namespaces явно + +`DocumentationAggregateTask` различает: + +- `structure.json`; +- `docs/**/*.md`; +- `assets/examples/**`; +- `meta/samples.json`; +- legacy `meta.json` и snippets без namespace. + +Неизвестные новые entries не интерпретируются как Kotlin snippets. Все ZIP paths проходят нормализацию и проверку, что target остаётся внутри рабочей/output директории. Совпадающие template paths из нескольких Core artifacts считаются ошибкой, потому что их автоматический merge не определён ADR. + +### 5. Structure является allowlist публичного content + +Aggregator читает navigation tree, проверяет каждый page path и насыщает только перечисленные страницы. Результат сохраняет относительный page path: + +```text +docs/components/ButtonUsage.md + ↓ +.sdds/temp/docs/content/components/ButtonUsage.md +``` + +Префикс `docs/` является корнем авторской документации и в output не дублируется. Markdown, отсутствующий в structure, остаётся служебным/черновым и не копируется. + +Core structure сохраняется в `.sdds/temp/docs/structure-core.json` рядом с будущим итоговым `docs.json`. Имя отражает, что это один из входов последующего merge, и не вводит лишнюю директорию `structure`. + +### 6. Используются существующие directives насыщения + +Новый template language не вводится. Kotlin directive `// @sample: ...`, XML directive `` и `` обрабатываются так же, как в текущем Docusaurus transformer. Сначала распаковываются Core examples, затем поверх них копируются локальные examples, после чего насыщается markdown. + +Неразрешённый directive является ошибкой: молчаливое сохранение placeholder создало бы некорректный публичный content. Ошибка содержит template path и sample reference. + +Screenshot directives сохраняются без изменений как стабильные ключи. PNG из `override-docs/static/screenshots-docusaurus` копируются в `assets/screenshots`, но локальные markdown-ссылки не создаются. Загрузка assets в S3 и замена ключей на S3 URL выполнятся documentation service и находятся за рамками change. Style API строится из platform `componentsInfoFile`. + +### 7. Docusaurus остаётся независимым legacy consumer + +`sidebars.ts`, common-template, portal config и `CHANGELOG.md` продолжают обслуживаться `convention.docusaurus`. Новый artifact pipeline не упаковывает static assets в Core docs JAR и не запускает npm/S3 задачи; platform aggregator копирует только локально созданные screenshots в итоговый assets namespace. Синхронизация `sidebars.ts` и `structure.json` обеспечивается тестом структуры относительно markdown allowlist, а не runtime-конвертацией. + +## Risks / Trade-offs + +- [Две навигационные модели могут разойтись] → добавить проверки, что paths из `structure.json` существуют, и ревьюить structure вместе с изменениями `sidebars.ts`/markdown. +- [Большой явный `structure.json` сложнее поддерживать, чем autogenerated sidebar] → считать явность частью publish contract; позже CLI сможет предложить безопасную команду structure update. +- [Переименование Gradle variant требует согласованного обновления producer и consumer] → изменить обе стороны в одном change и проверить variant resolution в Compose/View docs-модулях. +- [Существующие markdown содержат placeholders, не относящиеся к samples] → в этом change заменять только Kotlin/XML `@sample`; остальные placeholders остаются для последующих возможностей либо требуют отдельного согласованного processor. +- [Несколько Core artifacts могут содержать одинаковые paths] → fail fast вместо зависимости результата от порядка Gradle resolution. +- [Изменения convention затрагивают оба fixtures-модуля] → проверить содержимое обоих docs JAR и focused Compose/View aggregation tests. + +## Migration Plan + +1. Добавить и провалидировать оба `structure.json` без изменения Docusaurus pipeline. +2. Изменить layout docs JAR и добавить тесты содержимого/исключений. +3. Научить aggregator читать новый layout, сохранив legacy parsing. +4. Подключить enrichment и structure output. +5. Проверить Compose и View token docs modules, использующие `sddsCoreDocumentation`. + +Rollback состоит в возврате producer layout и aggregator routing; legacy parsing позволяет не требовать одновременного обновления уже собранных snippets-only JAR. + +Остальные Docusaurus placeholders не обрабатываются этим change и сохраняются без изменений. Их перенос в платформонезависимый enrichment требует отдельного контракта. diff --git a/openspec/changes/archive/2026-07-28-package-core-documentation-templates/proposal.md b/openspec/changes/archive/2026-07-28-package-core-documentation-templates/proposal.md new file mode 100644 index 0000000000..bcbd4dc946 --- /dev/null +++ b/openspec/changes/archive/2026-07-28-package-core-documentation-templates/proposal.md @@ -0,0 +1,32 @@ +## Why + +Core-документация Compose и View сейчас существует только как Docusaurus-шаблоны внутри `build-system` и не поставляется Android-инструментам как версионированный вход для DS Builder CLI. Необходимо публиковать markdown-шаблоны вместе с fixtures и насыщать их платформенными примерами в `.sdds/temp/docs/content`, сохраняя соответствие версии UIKit, примеров и документации. + +## What Changes + +- Добавить декларативные `structure.json` рядом с Compose- и View-шаблонами Docusaurus, воспроизводящие публичную навигацию существующих `sidebars.ts`. +- Расширить `convention.core-fixtures`, чтобы дополнительный docs-артефакт включал Core markdown, соответствующий `structure.json`, snippets и metadata. +- **BREAKING**: переименовать Gradle variant attribute документационного артефакта с `snippets` на `templates` у producer и consumer без публикации совместимого варианта. +- Ограничить шаблонную часть артефакта файлами `structure.json` и `docs/**/*.md`; Docusaurus-конфигурация, runtime-файлы и сгенерированный changelog в артефакт не попадают. +- Расширить documentation capability DS Builder Gradle Plugin: читать версионированные Core-шаблоны, подставлять Kotlin/XML snippets и Compose style API и записывать насыщенные markdown-файлы непосредственно в `.sdds/temp/docs/content`. +- Сохранять Core `structure.json` как `.sdds/temp/docs/structure-core.json` рядом с будущим `docs.json` для последующей сборки итоговой структуры средствами DS Builder CLI. +- Копировать локально созданные screenshots в `.sdds/temp/docs/assets/screenshots`, сохраняя в Markdown исходные screenshot keys для последующей замены documentation service на S3 URL. +- Сохранить текущую агрегацию examples, sample metadata и platform info files, включая совместимость с legacy snippets-only docs-артефактами. + +## Capabilities + +### New Capabilities + +- `core-documentation-templates`: Описывает декларативную структуру Compose/View Core-документации и состав версионированного docs-артефакта fixtures. + +### Modified Capabilities + +- `android-documentation-aggregation`: Расширяет платформенную агрегацию обработкой Core markdown-шаблонов и формированием насыщенного `content`. + +## Impact + +- `build-system`: изменятся Compose/View docs templates и `convention.core-fixtures`; потребуется проверка содержимого публикуемого `docs` JAR. +- `integration-core:uikit-compose-fixtures` и `integration-core:uikit-fixtures`: станут источниками версионированных Core documentation artifacts. +- `sdds-core:plugin_theme_builder`: изменятся распаковка и агрегация документации, формат входного Core docs-артефакта и тесты Gradle task. +- Изменение затрагивает Gradle conventions и docs generation, но не меняет API UIKit, токены или формат финального пакета сервиса документации. +- Валидация должна охватить unit-тесты плагина, сборку обоих fixtures docs JAR и Compose/View `documentationAggregate`. diff --git a/openspec/changes/archive/2026-07-28-package-core-documentation-templates/specs/android-documentation-aggregation/spec.md b/openspec/changes/archive/2026-07-28-package-core-documentation-templates/specs/android-documentation-aggregation/spec.md new file mode 100644 index 0000000000..b8c2469ef0 --- /dev/null +++ b/openspec/changes/archive/2026-07-28-package-core-documentation-templates/specs/android-documentation-aggregation/spec.md @@ -0,0 +1,94 @@ +## MODIFIED Requirements + +### Requirement: Core snippets принимаются как Gradle artifacts +Documentation capability SHALL предоставлять resolvable configuration для версионированных Core documentation artifacts и распаковывать их templates, snippets и metadata из `META-INF/sdds-docs` с проверкой безопасных относительных путей. + +#### Scenario: Подключён один или несколько core artifacts +- **WHEN** configuration Core documentation содержит JAR-артефакты с `META-INF/sdds-docs` +- **THEN** задача агрегации SHALL обработать документационные файлы всех артефактов в детерминированном порядке и SHALL завершиться понятной ошибкой при конфликтующих Core template paths + +#### Scenario: Core artifacts отсутствуют +- **WHEN** configuration Core documentation не содержит артефактов +- **THEN** локальные snippets и info-артефакты SHALL собираться без ошибки, а Core content SHALL оставаться пустым + +#### Scenario: Legacy Core artifact содержит meta.json +- **WHEN** Core artifact содержит `META-INF/sdds-docs/meta.json` +- **THEN** aggregator SHALL объединить его записи с локальной metadata в `.sdds/temp/docs/meta/samples.json` и SHALL NOT копировать `meta.json` как Kotlin snippet + +#### Scenario: Legacy Core artifact содержит snippets без template namespace +- **WHEN** Core artifact содержит прежний snippets-only layout внутри `META-INF/sdds-docs` +- **THEN** aggregator SHALL нормализовать snippets и metadata в текущий output layout без требования `structure.json` + +#### Scenario: Локальная metadata переопределяет Core sample +- **WHEN** Core и локальная metadata содержат одинаковый sample id +- **THEN** запись локальной metadata SHALL иметь приоритет + +#### Scenario: Core artifact содержит небезопасный путь +- **WHEN** путь ZIP entry выходит за пределы documentation output +- **THEN** задача MUST завершиться ошибкой и SHALL NOT записывать файл за пределами output + +### Requirement: Агрегированный результат пригоден для DS Builder CLI +Documentation capability SHALL производить платформенно насыщенный файловый результат с Gradle-declared inputs и outputs, который DS Builder CLI может использовать без знания исходных source sets, Docusaurus и задач Android-проекта. + +#### Scenario: Задача агрегации завершена +- **WHEN** выполняется lifecycle-задача агрегации документации +- **THEN** `.sdds/temp/docs` SHALL содержать насыщенный Core markdown непосредственно в `content`, Core structure в `structure-core.json`, snippets в `assets/examples/kotlin` и `assets/examples/xml`, screenshots в `assets/screenshots`, metadata в `meta/samples.json` и доступные platform info files в `meta` + +#### Scenario: Sample path разрешается от documentation root +- **WHEN** aggregator записывает Kotlin или XML sample metadata +- **THEN** `snippetPath` SHALL быть относительным к `.sdds/temp/docs` и начинаться с `assets/examples/kotlin/` или `assets/examples/xml/` + +#### Scenario: Входной файл отсутствует +- **WHEN** обязательный для выбранной платформы info-файл отсутствует +- **THEN** задача MUST завершиться ошибкой с точным путём отсутствующего файла + +#### Scenario: Core template отсутствует +- **WHEN** Core documentation artifact не содержит `structure.json` и markdown template +- **THEN** snippets и info-артефакты SHALL агрегироваться в legacy-compatible режиме без создания фиктивной структуры + +## ADDED Requirements + +### Requirement: Core markdown насыщается платформенными examples +Documentation capability SHALL обрабатывать существующие Kotlin/XML sample directives в Core markdown и записывать результат в `.sdds/temp/docs/content`, сохраняя относительные пути страниц. + +#### Scenario: Kotlin sample существует +- **WHEN** markdown содержит Kotlin `@sample` directive, разрешимый по Core или локальным snippets +- **THEN** directive SHALL быть заменён содержимым соответствующего Kotlin example + +#### Scenario: XML sample существует +- **WHEN** markdown содержит XML `@sample` directive, разрешимый по Core или локальным snippets +- **THEN** directive SHALL быть заменён содержимым соответствующего XML example + +#### Scenario: Локальный example переопределяет Core example +- **WHEN** Core и локальный example разрешаются по одному пути +- **THEN** при насыщении SHALL использоваться локальный example + +#### Scenario: Обязательный sample не разрешается +- **WHEN** markdown содержит `@sample` directive, для которого отсутствует example +- **THEN** задача MUST завершиться ошибкой с путём markdown-файла и значением directive + +### Requirement: Core structure сопровождает насыщенный content +Documentation capability SHALL сохранять `structure.json` из Core documentation artifact рядом с платформенным результатом в стабильном пути, доступном DS Builder CLI. + +#### Scenario: Core structure валидно ссылается на templates +- **WHEN** все пути страниц из `structure.json` существуют в Core template +- **THEN** aggregator SHALL скопировать структуру и создать соответствующие насыщенные файлы в `content` + +#### Scenario: Structure ссылается на отсутствующий markdown +- **WHEN** публичный path из `structure.json` отсутствует в Core template +- **THEN** задача MUST завершиться ошибкой с отсутствующим относительным путём + +#### Scenario: Template отсутствует в structure +- **WHEN** markdown-файл Core template не указан в `structure.json` +- **THEN** файл SHALL считаться непубличным и SHALL NOT попадать в `content` + +### Requirement: Style API насыщает markdown, а screenshots остаются внешним ресурсом +Documentation capability SHALL заменять существующие style-api directives платформенными данными, SHALL сохранять screenshot directives как ключи и SHALL копировать локальные screenshot assets для последующей публикации documentation service. + +#### Scenario: Markdown содержит screenshot directive +- **WHEN** markdown содержит `@screenshot` directive +- **THEN** aggregator SHALL сохранить directive без изменений, SHALL скопировать доступные PNG в `assets/screenshots` и SHALL NOT создавать локальную публичную ссылку + +#### Scenario: Compose style API существует +- **WHEN** markdown содержит `@style-api` directive и component info содержит style API +- **THEN** directive SHALL быть заменён таблицей параметров и примером выбора готового стиля diff --git a/openspec/changes/archive/2026-07-28-package-core-documentation-templates/specs/core-documentation-templates/spec.md b/openspec/changes/archive/2026-07-28-package-core-documentation-templates/specs/core-documentation-templates/spec.md new file mode 100644 index 0000000000..dc3081f4e6 --- /dev/null +++ b/openspec/changes/archive/2026-07-28-package-core-documentation-templates/specs/core-documentation-templates/spec.md @@ -0,0 +1,61 @@ +## ADDED Requirements + +### Requirement: Core-документация имеет платформонезависимую структуру +Compose- и View-шаблоны Core-документации SHALL содержать `structure.json`, явно описывающий публичную навигацию и пути markdown-страниц в соответствии с ADR-0003. + +#### Scenario: Compose structure создаётся из существующей навигации +- **WHEN** поставляется Compose Core documentation template +- **THEN** его `structure.json` SHALL содержать quick start, группы темы, визуальных эффектов и компонентов с явными ссылками на существующие Compose markdown-файлы + +#### Scenario: View structure создаётся из существующей навигации +- **WHEN** поставляется View Core documentation template +- **THEN** его `structure.json` SHALL содержать quick start, группы темы и компонентов, а также страницу focus с явными ссылками на существующие View markdown-файлы + +#### Scenario: Docusaurus использует отдельную навигацию +- **WHEN** Core template одновременно содержит `sidebars.ts` и `structure.json` +- **THEN** `sidebars.ts` SHALL оставаться входом legacy Docusaurus, а `structure.json` SHALL быть входом DS Builder CLI без runtime-зависимости между форматами + +### Requirement: Публичные страницы перечисляются явно +`structure.json` SHALL явно перечислять каждый публикуемый markdown-файл и SHALL NOT использовать Docusaurus `autogenerated` semantics. + +#### Scenario: Markdown входит в публичную структуру +- **WHEN** markdown-страница должна быть опубликована как Core-документация +- **THEN** относительный путь страницы SHALL присутствовать в дереве `navigation` + +#### Scenario: Генерируемый changelog отсутствует в исходном шаблоне +- **WHEN** `CHANGELOG.md` создаётся Docusaurus-задачей после копирования Core template +- **THEN** Core `structure.json` SHALL NOT ссылаться на `CHANGELOG.md` + +### Requirement: Fixtures публикуют ограниченный docs-артефакт +`convention.core-fixtures` SHALL публиковать дополнительный docs JAR, содержащий Core template, извлечённые examples и sample metadata в `META-INF/sdds-docs`. + +#### Scenario: Упаковывается Core template +- **WHEN** собирается `docsJar` Compose- или View-fixtures +- **THEN** шаблонная часть JAR SHALL содержать только соответствующий `structure.json` и файлы `docs/**/*.md` + +#### Scenario: Упаковываются examples и metadata +- **WHEN** задача извлечения snippets завершена +- **THEN** JAR SHALL содержать Kotlin/XML examples и sample metadata в отдельных предсказуемых путях внутри `META-INF/sdds-docs` + +#### Scenario: Docusaurus-файлы исключены +- **WHEN** собирается `docsJar` +- **THEN** JAR SHALL NOT содержать `sidebars.ts`, `docusaurus.config.ts`, package manager files, Docusaurus source/runtime files, static portal assets или сгенерированный `CHANGELOG.md` + +#### Scenario: В артефакте нет произвольных файлов template directory +- **WHEN** в Docusaurus template появляется новый файл, не совпадающий с `structure.json` или `docs/**/*.md` +- **THEN** этот файл SHALL NOT автоматически попадать в docs JAR + +### Requirement: Gradle variant представляет templates +Producer и consumer Core documentation artifact SHALL использовать значение `templates` атрибута `com.sdds.docs.variant`. + +#### Scenario: Fixtures публикуют docs variant +- **WHEN** `convention.core-fixtures` объявляет consumable documentation configuration +- **THEN** configuration SHALL иметь `com.sdds.docs.variant=templates` + +#### Scenario: DS Builder Plugin резолвит docs variant +- **WHEN** documentation capability создаёт resolvable Core documentation configuration +- **THEN** configuration SHALL запрашивать `com.sdds.docs.variant=templates` + +#### Scenario: Совместимый snippets variant не публикуется +- **WHEN** собираются outgoing variants fixtures +- **THEN** отдельный вариант с `com.sdds.docs.variant=snippets` SHALL NOT публиковаться diff --git a/openspec/changes/archive/2026-07-28-package-core-documentation-templates/tasks.md b/openspec/changes/archive/2026-07-28-package-core-documentation-templates/tasks.md new file mode 100644 index 0000000000..52208b6d34 --- /dev/null +++ b/openspec/changes/archive/2026-07-28-package-core-documentation-templates/tasks.md @@ -0,0 +1,38 @@ +## 1. Build system: структура Core-документации + +- [x] 1.1 Добавить `compose-template/structure.json` с явной навигацией quick start, theme, graphics и всех существующих component markdown, исключив `CHANGELOG.md` +- [x] 1.2 Добавить `xml-template/structure.json` с явной навигацией quick start, theme, components и focus, исключив `CHANGELOG.md` +- [x] 1.3 Добавить проверку, что все публичные paths обоих `structure.json` существуют, а неописанные markdown не попадают в публичный Core content случайно + +## 2. Build system и integration-core: docs-артефакт fixtures + +- [x] 2.1 Расширить `convention.core-fixtures`, выбрав Compose или View template root и добавив в `docsJar` только `structure.json` и `docs/**/*.md` +- [x] 2.2 Разместить извлечённые Kotlin/XML examples и `meta/samples.json` в явных namespaces `META-INF/sdds-docs/assets/examples/**` и `META-INF/sdds-docs/meta/samples.json` +- [x] 2.3 Сохранить classifier `docs`, переименовать Gradle variant attribute с `snippets` на `templates` у producer и не публиковать совместимый `snippets` variant +- [x] 2.4 Исключить из docs JAR `sidebars.ts`, Docusaurus runtime/config, static assets и генерируемый changelog +- [x] 2.5 Добавить проверки содержимого docs JAR для `integration-core:uikit-compose-fixtures` и `integration-core:uikit-fixtures`, включая attribute `templates` и отрицательные проверки Docusaurus-файлов + +## 3. sdds-core: чтение Core documentation artifact + +- [x] 3.1 Переименовать запрашиваемый `com.sdds.docs.variant` с `snippets` на `templates` в resolvable Core documentation configuration +- [x] 3.2 Разделить обработку entries в `DocumentationAggregateTask` на structure, markdown templates, namespaced examples, sample metadata и legacy snippets-only layout +- [x] 3.3 Добавить безопасную нормализацию ZIP paths и понятные ошибки для path traversal, конфликтующих template paths и некорректной структуры +- [x] 3.4 Сохранить merge Core/local sample metadata и приоритет локальных examples над Core examples +- [x] 3.5 Добавить unit-тесты нового layout и regression-тесты legacy `meta.json`/snippets behavior + +## 4. sdds-core: enrichment и output + +- [x] 4.1 Реализовать чтение navigation tree из Core `structure.json` и валидацию всех публичных markdown paths +- [x] 4.2 Реализовать замену существующих Kotlin `// @sample:` и XML `` directives с диагностикой template path и отсутствующего sample +- [x] 4.3 Записывать только перечисленные в structure насыщенные страницы в `.sdds/temp/docs/content` с сохранением относительных путей +- [x] 4.4 Записывать Core structure в `.sdds/temp/docs/structure-core.json`, сохраняя текущие `assets/examples` и `meta` outputs +- [x] 4.5 Обновить KDoc documentation task/plugin contract и тесты для empty Core artifact, конфликтов, missing pages и local sample override +- [x] 4.6 Сохранять screenshot directives как ключи, копировать PNG в `.sdds/temp/docs/assets/screenshots` и не создавать локальные ссылки; S3 enrichment оставить за рамками change +- [x] 4.7 Заменять Compose style-api directives данными из platform components info + +## 5. Интеграционная проверка + +- [x] 5.1 Собрать оба fixtures docs JAR и проверить их entries на соответствие allowlist +- [x] 5.2 Выполнить focused unit-тесты `sdds-core:plugin_theme_builder` +- [x] 5.3 Выполнить `documentationAggregate` минимум для одного Compose и одного View docs-модуля и проверить `content`, `structure-core.json`, examples, screenshots, сохранённые screenshot directives и meta +- [x] 5.4 Запустить релевантные detekt/Spotless проверки затронутых included builds из корня репозитория diff --git a/openspec/changes/archive/2026-07-28-unify-ds-builder-plugin/.openspec.yaml b/openspec/changes/archive/2026-07-28-unify-ds-builder-plugin/.openspec.yaml new file mode 100644 index 0000000000..8e7013b8b1 --- /dev/null +++ b/openspec/changes/archive/2026-07-28-unify-ds-builder-plugin/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-27 diff --git a/openspec/changes/archive/2026-07-28-unify-ds-builder-plugin/design.md b/openspec/changes/archive/2026-07-28-unify-ds-builder-plugin/design.md new file mode 100644 index 0000000000..dca1f57933 --- /dev/null +++ b/openspec/changes/archive/2026-07-28-unify-ds-builder-plugin/design.md @@ -0,0 +1,152 @@ +## Context + +`sdds-core/plugin_theme_builder` публикует Gradle-плагин генерации темы и компонентов. Извлечение documentation snippets находится в `build-system` в `convention.documentation*`, а генерация кода, связывающего дизайн-систему с sandbox infrastructure, — в `convention.integration-compose` и `convention.integration-view`. Эти conventions читают строковые Gradle properties и напрямую зависят от репозиторного build-system. + +Модули дизайн-системы имеют стандартную иерархию: основной Gradle project содержит `.sdds`, а `docs` и `integration` являются дочерними projects. `.sdds/config.json` содержит tenant-вариации темы; `config-info-*` и `theme-info-*` содержат платформенные машинные артефакты. В рамках ADR-0003 Android tool должен подготовить платформенно насыщенный локальный результат, а DS Builder CLI — объединить его с Core и пользовательской документацией и опубликовать итоговый пакет. + +Обратная совместимость Gradle API не требуется: единственные пользователи плагина — команда, которая одновременно мигрирует repository modules. + +## Goals / Non-Goals + +**Goals:** + +- заменить узкий `themeBuilder` единым `dsBuilder` DSL; +- держать theme, components, documentation и sandbox как независимые capabilities одного плагина; +- сделать `.sdds` общим источником conventions; +- перенести documentation и sandbox Gradle implementation из `build-system` в подпакеты `plugin_theme_builder`; +- убрать строковые integration properties и автоматически выводить стандартные пути, package и theme alias; +- мигрировать Compose и View модули `tokens` без требования переименовать их физические директории; +- обеспечить Gradle-declared inputs/outputs и focused tests для новой инфраструктуры. + +**Non-Goals:** + +- массово реорганизовывать существующие theme/component packages; +- сохранять `themeBuilder` или старый plugin API; +- переименовывать `plugin_theme_builder` Gradle project и все `integration` directories; +- включать Docusaurus, npm, changelog, S3 и публикацию в documentation aggregation; +- реализовывать DS Builder CLI merge/publish из ADR-0003; +- предоставлять documentation, sandbox или preview выбор tenant-вариации. + +## Decisions + +### 1. Новый plugin API называется DS Builder + +Plugin id становится `io.github.salute-developers.design-system-builder`, implementation class — `DsBuilderPlugin`, а extension — `dsBuilder`. Старые plugin id и extension не регистрируются. Новый DSL группирует настройки по capabilities: + +```kotlin +dsBuilder { + theme { /* existing generation options */ } + components { /* component source/options */ } + documentation { compose() } + sandbox { compose { /* overrides */ } } +} +``` + +Внутри `DsBuilderExtension` использует отдельные extension/model classes, а не превращается в один класс со всеми mutable полями. Существующие генераторы темы и компонентов на первом этапе адаптируются к новой модели с минимальным физическим переносом. + +Альтернатива — добавить documentation и sandbox внутрь `themeBuilder`. Она отклонена, потому что эти возможности не являются частями темы и могут применяться в отдельных Gradle projects. + +### 2. Один plugin artifact применяется отдельно в каждом project + +Основной, docs и integration/sandbox projects применяют один DS Builder plugin, но каждый включает только нужную capability. Плагин не ищет соседние projects по именам и не конфигурирует их из основного модуля. + +Это сохраняет Gradle project isolation и не связывает public API с layout `docs`/`integration`. Базовые `convention.android-lib`, `convention.compose` и testing conventions остаются ответственностью модулей; DS Builder plugin не применяет внутренние repo conventions. + +### 3. `.sdds` разрешается общим resolver + +`DsBuilderExtension` предоставляет `DirectoryProperty sddsDirectory`. Convention вычисляется лениво: + +1. явно заданная директория; +2. `.sdds` текущего project; +3. `.sdds` parent project. + +Все capabilities используют общий resolver. Стандартные platform files: + +| Platform | Components info | Theme info | +|---|---|---| +| Compose | `config-info-compose.json` | `theme-info-compose.json` | +| View | `config-info-view-system.json` | `theme-info-view-system.json` | + +Файлы View, которые сейчас находятся рядом с `.sdds`, мигрируют внутрь неё. Свойства называются `componentsInfoFile` и `themeInfoFile`, поскольку это машинные info-артефакты, а не Gradle configuration files. + +Альтернатива — требовать путь в каждом child project. Она отклонена как дублирование стандартной структуры. + +### 4. Theme alias описывает одну тему + +Tenants рассматриваются как вариации одной темы. Documentation, sandbox и будущий preview не получают tenant selector. Общий `themeAlias` выводится из alias первого базового tenant, с fallback на его name, как уже делает `SddsThemeSourceReader.baseAlias`. + +Sandbox допускает явный `themeAlias` override. Несколько tenant entries продолжают передаваться theme generator как default и дополнительные вариации, но наружу предоставляется одно имя темы. + +### 5. Sandbox заменяет integration в public API + +Новая capability и её классы используют термин `sandbox`, потому что результат связывает тему и компоненты с sandbox/demo infrastructure. Физические token-модули могут временно оставаться `integration`. + +Текущие `GenerateComponentsDictionary`, generator models и resource templates переносятся в `com.sdds.plugin.themebuilder.sandbox` и переименовываются по назначению. Typed properties заменяют: + +- `integration.compose.config-path`; +- `integration.compose.package-name`; +- `integration.compose.scheme`; +- `integration.compose.multiplatform`; +- `integration.view.config-path`; +- `integration.view.package-name`; +- `integration.view.scheme`; +- `theme-alias`. + +`generatedPackageName` по умолчанию берётся из Android namespace. Для non-Android/KMP fallback строится из `componentsInfo.packageName` по соглашению `.sandbox`. Generated sources пишутся в `build/generated/sdds/sandbox` и подключаются к Android/Kotlin source sets; запись в `src` прекращается. + +### 6. Documentation capability создаёт platform aggregation, а не портал + +В подпакет `documentation` переносятся snippet extraction, Kotlin compiler worker, XML extraction, metadata и unpack core snippets. Capability создаёт собственную resolvable configuration для core documentation artifacts и lifecycle task локальной агрегации. + +Результат содержит платформенные snippets, metadata, `components-info` и `theme-info` в стабильных относительных путях. Точная финальная структура пакета сервиса остаётся ответственностью DS Builder CLI. Docusaurus может временно потреблять новый aggregation output через существующий convention, но его build/deploy задачи не переносятся. + +### 7. Регистрация остаётся lazy и проверяемой + +Новые DSL properties реализуются через `Property`, `RegularFileProperty`, `DirectoryProperty` и Providers. Ошибки отсутствующих файлов возникают с точными resolved paths. Task inputs/outputs аннотируются для up-to-date checks и configuration cache. + +Public Gradle types и DSL получают актуальный KDoc. Для task registration и conventions добавляются Gradle TestKit tests; чистые readers/generators сохраняют unit tests. + +### 8. Android documentation output следует ADR-0003 + +Lifecycle-задача называется `documentationAggregate` и записывает платформенно насыщенный результат в +`.sdds/temp/docs`. Android snippets размещаются в `assets/examples/kotlin` и `assets/examples/xml`, metadata примеров — +в `meta/samples.json`, а platform info — в `meta/components-info.json` и `meta/theme-info.json`. +Legacy Core `META-INF/sdds-docs/meta.json` объединяется с локальной metadata; локальная запись имеет +приоритет при совпадении sample id. `snippetPath` нормализуется относительно `.sdds/temp/docs`. + +DS Builder Gradle Plugin не формирует `manifest.json` и `docs.json`: согласно ADR-0003 это ответственность +`dsbuilder docs generate`, который объединяет платформенный результат с Core и пользовательской документацией. + +### 9. Общие параметры генерации находятся на уровне dsBuilder + +`target`, `packageName`, `resourcePrefix`, `outputLocation` и `dimensions` задаются на уровне +`dsBuilder` и используются как conventions одновременно для theme и components capabilities. +Capability-level properties сохраняются как явные overrides для редких случаев, когда результаты +темы и компонентов должны различаться. Sandbox scheme по умолчанию — V2. + +## Risks / Trade-offs + +- **[Risk] Одновременный breaking DSL затрагивает много token-модулей** → мигрировать сначала по одному Compose и View пилоту, затем выполнить механическую миграцию остальных модулей и удалить conventions только после успешной проверки. +- **[Risk] Автопоиск parent `.sdds` скрывает ошибочный layout** → ограничить поиск текущим и непосредственным parent project, логировать resolved directory и поддержать явный override. +- **[Risk] Android namespace может отличаться от исторического generated package** → проверить каждый существующий integration module; использовать явный override только для необходимых исключений. +- **[Risk] Перенос Kotlin compiler extraction увеличит зависимости plugin artifact** → сохранить compiler в отдельной resolvable classpath configuration и worker classloader isolation, не загружать compiler в Gradle daemon classpath без необходимости. +- **[Risk] Перемещение View info files ломает текущие пути** → выполнять перенос и migration DSL атомарно, покрыть Compose/View resolver tests. +- **[Trade-off] Физические имена `plugin_theme_builder` и `integration` временно расходятся с public terminology** → принять как ограничение первого этапа и не смешивать архитектурную миграцию с массовым переименованием directories/coordinates. + +## Migration Plan + +1. Добавить `DsBuilderPlugin`, composed extension models и общий `.sdds` resolver в `sdds-core/plugin_theme_builder`. +2. Перевести существующую theme/components регистрацию на `dsBuilder`, сохранив реализации генераторов. +3. Перенести documentation task types и добавить aggregation lifecycle/API. +4. Мигрировать один Compose и один View docs project; подключить существующий Docusaurus pipeline к новому output при необходимости. +5. Перенести sandbox generator, models и templates; добавить generated source wiring. +6. Мигрировать один Compose и один View integration project и сравнить generated output. +7. Мигрировать оставшиеся `tokens` projects, удалить obsolete properties и специализированные conventions. +8. Обновить README/KDoc и выполнить focused, token-wide и repository validation. + +Rollback выполняется возвратом change целиком до публикации новой версии плагина; параллельный legacy API намеренно не поддерживается. + +## Open Questions + +- Должна ли documentation capability сама применять KSP plugin или только конфигурировать его при наличии в project? +- Нужен ли отдельный published artifact/configuration для передачи aggregation output в CLI, либо на первом этапе достаточно документированной output directory? diff --git a/openspec/changes/archive/2026-07-28-unify-ds-builder-plugin/proposal.md b/openspec/changes/archive/2026-07-28-unify-ds-builder-plugin/proposal.md new file mode 100644 index 0000000000..17a5203923 --- /dev/null +++ b/openspec/changes/archive/2026-07-28-unify-ds-builder-plugin/proposal.md @@ -0,0 +1,35 @@ +## Why + +Генерация темы и компонентов находится в публикуемом `plugin_theme_builder`, а сбор документации и генерация адаптеров песочницы реализованы внутренними convention-плагинами `build-system`. Из-за этого единый процесс DS Builder распределён между разными Gradle API, строковыми properties и репозиторными соглашениями, хотя все возможности используют одни и те же артефакты `.sdds`. + +## What Changes + +- **BREAKING**: заменить публичный extension `themeBuilder {}` на единый `dsBuilder {}` и переименовать implementation плагина без compatibility-слоя. +- Разделить возможности `dsBuilder` на независимые области `theme`, `components`, `documentation` и `sandbox`, не выполняя масштабного переноса существующих theme/component классов и пакетов. +- Перенести сбор платформенных фрагментов документации и связанных Gradle tasks из documentation convention-плагинов в подпакет `documentation` модуля `sdds-core/plugin_theme_builder`. +- Перенести генерацию Compose/View адаптеров из integration convention-плагинов в подпакет `sandbox` модуля `sdds-core/plugin_theme_builder`. +- Заменить `integration.*` и `theme-alias` Gradle properties типизированным sandbox DSL. +- Использовать `.sdds` как источник стандартных `config.json`, `config-info-*` и `theme-info-*`; разрешать явные пути только как overrides. +- Автоматически выводить пакет генерируемого sandbox-кода и `themeAlias`, сохраняя возможность явного переопределения. +- Удалить специализированные documentation/integration convention-плагины после миграции потребляющих модулей `tokens`. +- Не переносить в агрегатор portal-specific Docusaurus build, npm, changelog, S3 deployment и публикацию. + +## Capabilities + +### New Capabilities + +- `ds-builder-gradle-dsl`: единый типизированный `dsBuilder` DSL, общая резолюция директории `.sdds` и активация независимых возможностей плагина. +- `android-documentation-aggregation`: сбор Kotlin/XML snippets и платформенных info-артефактов в результат, пригодный для дальнейшей упаковки DS Builder CLI. +- `sandbox-adapter-generation`: генерация Compose и View адаптеров дизайн-системы для sandbox/demo infrastructure из `.sdds` metadata. + +### Modified Capabilities + +- `theme-builder-dsbuilder-source`: fallback источников темы должен работать через новый `dsBuilder` API и общую резолюцию `.sdds`, сохраняя модель нескольких tenant-вариаций одной темы. + +## Impact + +- `sdds-core/plugin_theme_builder`: новый публичный Gradle DSL, новые подпакеты `documentation` и `sandbox`, перенос task types, моделей и templates. +- `build-system/conventions`: удаление documentation/integration conventions и перенос принадлежащей им логики; базовые Android, Compose, testing и Docusaurus conventions остаются отдельными. +- `tokens`: миграция theme, documentation и integration-модулей на новый plugin id/DSL; физические имена `integration`-модулей на этом этапе могут сохраниться. +- Публичный Gradle API меняется несовместимо, но внешняя совместимость не требуется, поскольку плагин используется только командой разработки. +- Изменение затрагивает генерацию документации и sandbox-кода, поэтому требует focused validation для плагина и пилотных Compose/View модулей, а затем проверки всех затронутых модулей `tokens`. diff --git a/openspec/changes/archive/2026-07-28-unify-ds-builder-plugin/specs/android-documentation-aggregation/spec.md b/openspec/changes/archive/2026-07-28-unify-ds-builder-plugin/specs/android-documentation-aggregation/spec.md new file mode 100644 index 0000000000..e166c4e3f4 --- /dev/null +++ b/openspec/changes/archive/2026-07-28-unify-ds-builder-plugin/specs/android-documentation-aggregation/spec.md @@ -0,0 +1,53 @@ +## ADDED Requirements + +### Requirement: Плагин собирает платформенные фрагменты документации +Documentation capability SHALL собирать Kotlin и XML snippets, metadata и платформенные info-артефакты в объявленные Gradle outputs. + +#### Scenario: Compose documentation +- **WHEN** модуль включает `documentation { compose() }` +- **THEN** плагин SHALL зарегистрировать задачи извлечения Kotlin snippets и включить Compose `componentsInfoFile` и `themeInfoFile` в агрегированный результат + +#### Scenario: View documentation +- **WHEN** модуль включает `documentation { view() }` +- **THEN** плагин SHALL зарегистрировать задачи извлечения Kotlin/XML snippets и включить View `componentsInfoFile` и `themeInfoFile` в агрегированный результат + +### Requirement: Core snippets принимаются как Gradle artifacts +Documentation capability SHALL предоставлять resolvable configuration для артефактов core snippets и распаковывать содержимое `META-INF/sdds-docs` в директорию агрегирования. + +#### Scenario: Подключён один или несколько core artifacts +- **WHEN** configuration core snippets содержит JAR-артефакты с `META-INF/sdds-docs` +- **THEN** задача агрегации SHALL распаковать документационные файлы всех артефактов в общий output с детерминированным поведением при совпадении путей + +#### Scenario: Core artifacts отсутствуют +- **WHEN** configuration core snippets не содержит артефактов +- **THEN** локальные snippets и info-артефакты SHALL собираться без ошибки + +#### Scenario: Legacy Core artifact содержит meta.json +- **WHEN** Core artifact содержит `META-INF/sdds-docs/meta.json` +- **THEN** aggregator SHALL объединить его записи с локальной metadata в `.sdds/temp/docs/meta/samples.json` и SHALL NOT копировать `meta.json` как Kotlin snippet + +#### Scenario: Локальная metadata переопределяет Core sample +- **WHEN** Core и локальная metadata содержат одинаковый sample id +- **THEN** запись локальной metadata SHALL иметь приоритет + +### Requirement: Агрегированный результат пригоден для DS Builder CLI +Documentation capability SHALL производить платформенно насыщенный файловый результат с Gradle-declared inputs и outputs, который DS Builder CLI может использовать без знания исходных source sets и задач Android-проекта. + +#### Scenario: Задача агрегации завершена +- **WHEN** выполняется lifecycle-задача агрегации документации +- **THEN** `.sdds/temp/docs` SHALL содержать snippets в `assets/examples/kotlin` и `assets/examples/xml`, metadata в `meta/samples.json` и доступные platform info files в `meta` + +#### Scenario: Sample path разрешается от documentation root +- **WHEN** aggregator записывает Kotlin или XML sample metadata +- **THEN** `snippetPath` SHALL быть относительным к `.sdds/temp/docs` и начинаться с `assets/examples/kotlin/` или `assets/examples/xml/` + +#### Scenario: Входной файл отсутствует +- **WHEN** обязательный для выбранной платформы info-файл отсутствует +- **THEN** задача MUST завершиться ошибкой с точным путём отсутствующего файла + +### Requirement: Portal build и публикация не входят в capability +Documentation capability SHALL NOT выполнять Docusaurus template generation, npm-команды, changelog synchronization, S3 upload или публикацию документации. + +#### Scenario: Выполняется агрегация документации +- **WHEN** запускается lifecycle-задача documentation capability +- **THEN** она SHALL завершаться созданием локального платформенного результата и SHALL NOT запускать portal build или network publication diff --git a/openspec/changes/archive/2026-07-28-unify-ds-builder-plugin/specs/ds-builder-gradle-dsl/spec.md b/openspec/changes/archive/2026-07-28-unify-ds-builder-plugin/specs/ds-builder-gradle-dsl/spec.md new file mode 100644 index 0000000000..9971d6ce32 --- /dev/null +++ b/openspec/changes/archive/2026-07-28-unify-ds-builder-plugin/specs/ds-builder-gradle-dsl/spec.md @@ -0,0 +1,69 @@ +## ADDED Requirements + +### Requirement: Единый публичный DS Builder DSL +Gradle-плагин с id `io.github.salute-developers.design-system-builder` SHALL регистрировать extension `dsBuilder` как единую точку конфигурации генерации темы, компонентов, документации и sandbox-адаптеров. + +#### Scenario: Проект конфигурирует одну возможность +- **WHEN** проект применяет DS Builder Gradle Plugin и конфигурирует только один из блоков `theme`, `components`, `documentation` или `sandbox` +- **THEN** плагин SHALL регистрировать и настраивать задачи этой возможности без требования сконфигурировать остальные блоки + +#### Scenario: Старый extension отсутствует +- **WHEN** проект применяет новую версию DS Builder Gradle Plugin +- **THEN** extension `themeBuilder` SHALL NOT регистрироваться и проект MUST использовать `dsBuilder` + +#### Scenario: Старый plugin id отсутствует +- **WHEN** repository modules мигрированы на DS Builder Gradle Plugin +- **THEN** они SHALL применять `io.github.salute-developers.design-system-builder` и SHALL NOT применять `io.github.salute-developers.theme-builder-plugin` + +### Requirement: Общая резолюция директории .sdds +`dsBuilder` SHALL предоставлять общее свойство `sddsDirectory` и автоматически находить `.sdds` для текущего или родительского проекта. + +#### Scenario: .sdds находится в текущем проекте +- **WHEN** `sddsDirectory` не задан явно и `${project.projectDir}/.sdds` существует +- **THEN** плагин SHALL использовать эту директорию + +#### Scenario: .sdds находится в родительском проекте +- **WHEN** `sddsDirectory` не задан явно, `.sdds` отсутствует в текущем проекте и `${project.parent.projectDir}/.sdds` существует +- **THEN** плагин SHALL использовать `.sdds` родительского проекта + +#### Scenario: Директория переопределена +- **WHEN** пользователь явно задаёт `sddsDirectory` +- **THEN** все включённые возможности SHALL разрешать стандартные DS Builder артефакты относительно указанной директории + +#### Scenario: Директория не найдена +- **WHEN** возможность требует локальные DS Builder артефакты, `sddsDirectory` не задана и `.sdds` не найдена в текущем или родительском проекте +- **THEN** конфигурация или выполнение соответствующей задачи MUST завершиться понятной ошибкой со списком проверенных путей + +### Requirement: Платформенные info-файлы имеют стандартные conventions +Плагин SHALL выводить стандартные `componentsInfoFile` и `themeInfoFile` из разрешённой `.sdds` директории и выбранной платформы. + +#### Scenario: Выбрана Compose-платформа +- **WHEN** documentation или sandbox capability настроена для Compose и пути не переопределены +- **THEN** `componentsInfoFile` SHALL указывать на `.sdds/config-info-compose.json`, а `themeInfoFile` SHALL указывать на `.sdds/theme-info-compose.json` + +#### Scenario: Выбрана View-платформа +- **WHEN** documentation или sandbox capability настроена для View и пути не переопределены +- **THEN** `componentsInfoFile` SHALL указывать на `.sdds/config-info-view-system.json`, а `themeInfoFile` SHALL указывать на `.sdds/theme-info-view-system.json` + +#### Scenario: Info-файл переопределён +- **WHEN** пользователь явно задаёт `componentsInfoFile` или `themeInfoFile` +- **THEN** соответствующая capability SHALL использовать явно заданный файл вместо стандартного пути + +### Requirement: Возможности плагина не зависят от внутренних convention-плагинов +DS Builder Gradle Plugin SHALL регистрировать собственные configurations, task types и platform-specific настройки documentation и sandbox capabilities без применения специализированных `convention.documentation-*` или `convention.integration-*`. + +#### Scenario: Плагин используется в token-модуле +- **WHEN** token-модуль применяет базовые Android/Compose conventions и DS Builder Gradle Plugin +- **THEN** documentation или sandbox capability SHALL работать без специализированного documentation/integration convention-плагина + +### Requirement: Theme и components наследуют общие generation settings +`dsBuilder` SHALL предоставлять общие `target`, `packageName`, `resourcePrefix`, `outputLocation` +и `dimensions`, применяемые как conventions к theme и components capabilities. + +#### Scenario: Общие dimensions используют Android resources +- **WHEN** пользователь включает `dimensions.fromResources` на уровне `dsBuilder` +- **THEN** и theme, и components generation SHALL получить `DimensionsConfig.fromResources = true` + +#### Scenario: Capability переопределяет общее значение +- **WHEN** theme или components задаёт собственное generation property +- **THEN** явно заданное capability value SHALL иметь приоритет над общей convention diff --git a/openspec/changes/archive/2026-07-28-unify-ds-builder-plugin/specs/sandbox-adapter-generation/spec.md b/openspec/changes/archive/2026-07-28-unify-ds-builder-plugin/specs/sandbox-adapter-generation/spec.md new file mode 100644 index 0000000000..44aff873c5 --- /dev/null +++ b/openspec/changes/archive/2026-07-28-unify-ds-builder-plugin/specs/sandbox-adapter-generation/spec.md @@ -0,0 +1,67 @@ +## ADDED Requirements + +### Requirement: Sandbox capability генерирует платформенные адаптеры +Sandbox capability SHALL генерировать адаптеры темы и компонентов для sandbox/demo infrastructure из `componentsInfoFile`. + +#### Scenario: Compose sandbox +- **WHEN** модуль включает `sandbox { compose { ... } }` +- **THEN** плагин SHALL зарегистрировать Compose sandbox generation task с Compose target и выбранной схемой + +#### Scenario: View sandbox +- **WHEN** модуль включает `sandbox { view { ... } }` +- **THEN** плагин SHALL зарегистрировать View sandbox generation task с View target и выбранной схемой + +### Requirement: Sandbox inputs имеют типизированные conventions +Sandbox extension SHALL предоставлять типизированные Gradle properties для `componentsInfoFile`, `generatedPackageName`, `themeAlias`, `scheme` и Compose Multiplatform mode. + +#### Scenario: Стандартный components info +- **WHEN** `componentsInfoFile` не задан явно +- **THEN** sandbox capability SHALL использовать platform-specific файл из разрешённой `.sdds` + +#### Scenario: Package выводится из Android namespace +- **WHEN** `generatedPackageName` не задан явно и текущий Android-модуль имеет `namespace` +- **THEN** generator SHALL использовать Android namespace как пакет генерируемого кода + +#### Scenario: Package выводится без Android namespace +- **WHEN** `generatedPackageName` и Android namespace отсутствуют, но `componentsInfoFile` содержит `packageName` +- **THEN** generator SHALL вывести sandbox package из `componentsInfoFile.packageName` по документированному соглашению + +#### Scenario: Theme alias выводится из .sdds config +- **WHEN** `themeAlias` не задан явно +- **THEN** generator SHALL использовать alias первого базового tenant из `.sdds/config.json`, а при отсутствии alias — его name + +#### Scenario: Theme alias переопределён +- **WHEN** пользователь явно задаёт `themeAlias` +- **THEN** generator SHALL использовать явно заданное название темы + +### Requirement: Tenants не выбираются sandbox capability +Sandbox capability SHALL рассматривать tenants как вариации одной темы и SHALL NOT предоставлять параметр выбора tenant. + +#### Scenario: Config содержит несколько tenants +- **WHEN** `.sdds/config.json` содержит несколько tenant-вариаций +- **THEN** sandbox configuration SHALL разрешить одно название темы и SHALL NOT требовать выбора tenant + +### Requirement: Generated sources подключаются к сборке +Sandbox generator SHALL создавать код в Gradle build directory и подключать output directory к соответствующему Kotlin source set. + +#### Scenario: Android Compose или View module +- **WHEN** sandbox generation task создаёт код для Android-модуля +- **THEN** generated directory SHALL участвовать в компиляции без записи в `src/main` + +#### Scenario: Compose Multiplatform module +- **WHEN** Compose sandbox включает multiplatform mode +- **THEN** generated directory SHALL подключаться к согласованному common Kotlin source set + +### Requirement: Старые integration properties не используются +Sandbox capability SHALL NOT читать `integration.compose.*`, `integration.view.*` или `theme-alias` из Gradle properties. + +#### Scenario: Модуль мигрирован на sandbox DSL +- **WHEN** sandbox module применяет новый DS Builder Plugin +- **THEN** вся конфигурация generator SHALL поступать из typed extension properties и `.sdds` metadata + +### Requirement: Sandbox schema по умолчанию использует V2 +Sandbox capability SHALL использовать `SandboxScheme.V2`, когда schema явно не переопределена. + +#### Scenario: Schema не указана +- **WHEN** пользователь включает Compose или View sandbox без явного `scheme` +- **THEN** generation task SHALL использовать `SandboxScheme.V2` diff --git a/openspec/changes/archive/2026-07-28-unify-ds-builder-plugin/specs/theme-builder-dsbuilder-source/spec.md b/openspec/changes/archive/2026-07-28-unify-ds-builder-plugin/specs/theme-builder-dsbuilder-source/spec.md new file mode 100644 index 0000000000..2c8f00e68b --- /dev/null +++ b/openspec/changes/archive/2026-07-28-unify-ds-builder-plugin/specs/theme-builder-dsbuilder-source/spec.md @@ -0,0 +1,46 @@ +## MODIFIED Requirements + +### Requirement: Fallback source from .sdds config +`sdds-core/plugin_theme_builder` SHALL use the `.sdds/config.json` resolved by `dsBuilder.sddsDirectory` as a fallback theme source when `dsBuilder.theme` does not define an explicit source. + +#### Scenario: Extension has no explicit theme source +- **WHEN** a project applies the DS Builder Gradle Plugin, enables `dsBuilder.theme`, does not configure an explicit source, and the resolved `.sdds/config.json` exists +- **THEN** the theme capability resolves theme sources from `.sdds/config.json` + +#### Scenario: Extension has explicit theme source +- **WHEN** a project configures an explicit source in `dsBuilder.theme` +- **THEN** the theme capability uses the explicit source and does not replace it with `.sdds/config.json` sources + +### Requirement: DSBuilder tenants become legacy-compatible theme sources +The theme capability SHALL convert every tenant from `.sdds/config.json` into a local variation source of one theme using `tenant.alias` when present, otherwise `tenant.name`. + +#### Scenario: First tenant becomes internal default +- **WHEN** `.sdds/config.json` contains at least one tenant +- **THEN** the theme capability passes the first tenant to `GenerateThemeTask` with an empty tenant suffix so existing `Tenant.Default` behavior is preserved + +#### Scenario: Non-first tenants use public tenant names +- **WHEN** `.sdds/config.json` contains tenants after the first tenant +- **THEN** the theme capability passes each additional tenant to `GenerateThemeTask` with tenant suffix equal to `tenant.alias` when present, otherwise `tenant.name` + +#### Scenario: Base theme name comes from first tenant +- **WHEN** the theme capability resolves sources from `.sdds/config.json` +- **THEN** it uses the first tenant `alias` when present, otherwise `name`, as the single base theme name exposed to documentation, sandbox and preview capabilities + +### Requirement: Invalid .sdds config fails clearly +The theme capability SHALL fail with a clear error when the resolved `.sdds/config.json` is required as fallback but cannot provide usable theme sources. + +#### Scenario: Missing config and no explicit source +- **WHEN** `dsBuilder.theme` does not configure an explicit source and the resolved `.sdds/config.json` does not exist +- **THEN** the theme capability fails with a message explaining that an explicit source or `.sdds/config.json` must be provided + +#### Scenario: Empty tenants +- **WHEN** `.sdds/config.json` exists but contains no tenants +- **THEN** the theme capability fails with a message explaining that `.sdds/config.json` must contain at least one tenant + +#### Scenario: Missing tenant files +- **WHEN** a resolved local tenant directory does not contain required `meta.json` or `android/android_*.json` files +- **THEN** the theme capability fails with a message that includes the missing file path + +#### Scenario: Missing palette file +- **WHEN** the theme capability resolves sources from `.sdds/config.json` and the resolved palette file does not exist +- **THEN** the theme capability fails with a message that includes the missing palette file path diff --git a/openspec/changes/archive/2026-07-28-unify-ds-builder-plugin/tasks.md b/openspec/changes/archive/2026-07-28-unify-ds-builder-plugin/tasks.md new file mode 100644 index 0000000000..83b31fad2f --- /dev/null +++ b/openspec/changes/archive/2026-07-28-unify-ds-builder-plugin/tasks.md @@ -0,0 +1,64 @@ +## 1. DS Builder plugin foundation (`sdds-core/plugin_theme_builder`) + +- [x] 1.1 Зарегистрировать plugin id `io.github.salute-developers.design-system-builder`, implementation `DsBuilderPlugin` и extension `dsBuilder`, удалив старую регистрацию `themeBuilder` +- [x] 1.2 Реализовать composed extension models для `theme`, `components`, `documentation` и `sandbox` с типизированными Gradle properties и KDoc +- [x] 1.3 Реализовать общий lazy resolver `sddsDirectory`, `.sdds/config.json`, platform-specific `componentsInfoFile` и `themeInfoFile` с точными диагностическими сообщениями +- [x] 1.4 Перевести регистрацию существующих theme/components задач на новый DSL без массового переноса генераторов и подтвердить сохранение tenant-вариаций одной темы +- [x] 1.5 Добавить unit/TestKit tests для plugin id, отсутствия legacy DSL, независимой активации capabilities и поиска `.sdds` в текущем/parent project + +## 2. Documentation capability (`sdds-core/plugin_theme_builder`) + +- [x] 2.1 Перенести snippet extractor, Kotlin compiler worker, XML extractor, metadata models и unzip task из `build-system` в подпакет `documentation` +- [x] 2.2 Добавить documentation extension для Compose/View, resolvable core snippets configuration и standard output conventions +- [x] 2.3 Реализовать lifecycle-задачу платформенной агрегации snippets, metadata, components info и theme info с declared inputs/outputs +- [x] 2.4 Обеспечить детерминированное объединение core snippet artifacts и понятные ошибки отсутствующих обязательных info-файлов +- [x] 2.5 Добавить unit/TestKit tests для Compose/View conventions, пустых и нескольких core artifacts, aggregation layout и отсутствия portal/network задач в lifecycle graph + +## 3. Sandbox capability (`sdds-core/plugin_theme_builder`) + +- [x] 3.1 Перенести `GenerateComponentsDictionary`, generator models и Compose/View templates из `build-system` в подпакет `sandbox` и переименовать API по новому назначению +- [x] 3.2 Реализовать `sandbox { compose { ... } / view { ... } }` с properties `componentsInfoFile`, `generatedPackageName`, `themeAlias`, `scheme` и multiplatform mode +- [x] 3.3 Реализовать conventions для package из Android namespace или `componentsInfo.packageName` и для `themeAlias` из базового tenant `.sdds/config.json` +- [x] 3.4 Перенести generated output в `build/generated/sdds/sandbox` и подключить его к Android и Compose Multiplatform Kotlin source sets +- [x] 3.5 Добавить unit/TestKit tests для Compose/View generation, package/theme alias conventions, нескольких tenant-вариаций без selector и generated source wiring + +## 4. Пилотная миграция (`tokens`, `build-system`) + +- [x] 4.1 Перенести View `config-info`/`theme-info` пилотной дизайн-системы в `.sdds` и мигрировать один Compose и один View theme module на новый plugin id и `dsBuilder` +- [x] 4.2 Мигрировать один Compose и один View docs module с `convention.documentation-*` на documentation capability и проверить существующий Docusaurus consumer +- [x] 4.3 Мигрировать один Compose и один View integration module на sandbox capability, удалить их `integration.*`/`theme-alias` properties и сравнить сгенерированный API +- [x] 4.4 После успешной пилотной проверки удалить перенесённую implementation-логику из documentation/integration conventions либо превратить необходимые переходные места во внутренние consumers нового plugin API + +## 5. Полная миграция и cleanup (`tokens`, `build-system`, `sdds-core`) + +- [x] 5.1 Мигрировать оставшиеся theme/component modules `tokens` с `themeBuilder` на `dsBuilder` +- [x] 5.2 Мигрировать оставшиеся docs modules на documentation capability и стандартные `.sdds` info paths +- [x] 5.3 Мигрировать оставшиеся integration modules на sandbox capability, сохранив физические имена modules там, где переименование не требуется +- [x] 5.4 Удалить `convention.documentation`, `convention.documentation-compose`, `convention.documentation-view`, `convention.integration-compose`, `convention.integration-view` и неиспользуемые extension/task/resources из `build-system` +- [x] 5.5 Удалить obsolete Gradle properties и обновить version catalog/plugin references на новый plugin id + +## 6. Документация и проверка + +- [x] 6.1 Обновить README `plugin_theme_builder`, примеры Gradle DSL и KDoc для `.sdds`, documentation и sandbox conventions +- [x] 6.2 Выполнить focused unit/TestKit, detekt и Spotless проверки `sdds-core/plugin_theme_builder` +- [x] 6.3 Собрать пилотные и затем все затронутые Compose/View theme, docs и sandbox modules из корня репозитория +- [x] 6.4 Выполнить полные проверки, безопасные для локального запуска: `./gradlew detektAll` и `./gradlew spotlessApplyAll`; `testAll` не запускать, поскольку он включает screenshot-тесты `tokens`. Зафиксировать отдельно инфраструктурные ограничения полного composite build + - `spotlessApplyAll` завершён успешно. + - `detektAll` запускается, но падает на накопленных нарушениях в сгенерированных `tokens` sources (`MaxLineLength`, `UndocumentedPublicFunction`); целевая проверка `plugin_theme_builder:detekt` проходит. + +## 7. Исправления после review + +- [x] 7.1 Поднять общие target, packageName, resourcePrefix, outputLocation и dimensions на уровень `dsBuilder`, сохранив capability overrides +- [x] 7.2 Мигрировать theme/components consumers на общие generation settings и применить `dimensions.fromResources` к обоим генераторам `plasma-stards-compose` +- [x] 7.3 Изменить default sandbox schema на V2 и покрыть новые conventions тестами + +## 8. Исправление documentation enrichment layout + +- [x] 8.1 Использовать `.sdds/temp/docs` как корень `documentationAggregate` и обновить всех consumers +- [x] 8.2 Объединять Core `meta.json` и локальную snippet metadata в `meta/samples.json`, сохраняя приоритет локальных записей +- [x] 8.3 Добавить regression tests для Core metadata и итогового layout +- [x] 8.4 Нормализовать Kotlin/XML `snippetPath` относительно `.sdds/temp/docs` + +## 9. Исправление активации генерации темы + +- [x] 9.1 Включить theme capability во всех корневых `tokens/{ds-module}` проектах, которые до миграции генерировали и тему, и компоненты diff --git a/openspec/specs/android-documentation-aggregation/spec.md b/openspec/specs/android-documentation-aggregation/spec.md new file mode 100644 index 0000000000..1933f8982f --- /dev/null +++ b/openspec/specs/android-documentation-aggregation/spec.md @@ -0,0 +1,148 @@ +# android-documentation-aggregation Specification + +## Purpose +TBD - created by archiving change unify-ds-builder-plugin. Update Purpose after archive. +## Requirements +### Requirement: Плагин собирает платформенные фрагменты документации +Documentation capability SHALL собирать Kotlin и XML snippets, metadata и платформенные info-артефакты в объявленные Gradle outputs. + +#### Scenario: Compose documentation +- **WHEN** модуль включает `documentation { compose() }` +- **THEN** плагин SHALL зарегистрировать задачи извлечения Kotlin snippets и включить Compose `componentsInfoFile` и `themeInfoFile` в агрегированный результат + +#### Scenario: View documentation +- **WHEN** модуль включает `documentation { view() }` +- **THEN** плагин SHALL зарегистрировать задачи извлечения Kotlin/XML snippets и включить View `componentsInfoFile` и `themeInfoFile` в агрегированный результат + +### Requirement: Core snippets принимаются как Gradle artifacts +Documentation capability SHALL предоставлять resolvable configuration для версионированных Core documentation artifacts и распаковывать их templates, snippets и metadata из `META-INF/sdds-docs` с проверкой безопасных относительных путей. + +#### Scenario: Подключён один или несколько core artifacts +- **WHEN** configuration Core documentation содержит JAR-артефакты с `META-INF/sdds-docs` +- **THEN** задача агрегации SHALL обработать документационные файлы всех артефактов в детерминированном порядке и SHALL завершиться понятной ошибкой при конфликтующих Core template paths + +#### Scenario: Core artifacts отсутствуют +- **WHEN** configuration Core documentation не содержит артефактов +- **THEN** локальные snippets и info-артефакты SHALL собираться без ошибки, а Core content SHALL оставаться пустым + +#### Scenario: Legacy Core artifact содержит meta.json +- **WHEN** Core artifact содержит `META-INF/sdds-docs/meta.json` +- **THEN** aggregator SHALL объединить его записи с локальной metadata в `.sdds/temp/docs/meta/samples.json` и SHALL NOT копировать `meta.json` как Kotlin snippet + +#### Scenario: Legacy Core artifact содержит snippets без template namespace +- **WHEN** Core artifact содержит прежний snippets-only layout внутри `META-INF/sdds-docs` +- **THEN** aggregator SHALL нормализовать snippets и metadata в текущий output layout без требования `structure.json` + +#### Scenario: Локальная metadata переопределяет Core sample +- **WHEN** Core и локальная metadata содержат одинаковый sample id +- **THEN** запись локальной metadata SHALL иметь приоритет + +#### Scenario: Core artifact содержит небезопасный путь +- **WHEN** путь ZIP entry выходит за пределы documentation output +- **THEN** задача MUST завершиться ошибкой и SHALL NOT записывать файл за пределами output + +### Requirement: Агрегированный результат пригоден для DS Builder CLI +Documentation capability SHALL производить раздельный платформенно насыщенный Core и user результат с Gradle-declared inputs и outputs, который DS Builder CLI может объединить без знания исходных source sets, Docusaurus и задач Android-проекта. + +#### Scenario: Задача агрегации завершена +- **WHEN** выполняется lifecycle-задача агрегации документации с Core и user structures +- **THEN** `.sdds/temp/docs` SHALL содержать Core markdown в `content/core`, user markdown в `content/user`, `structure-core.json`, `structure-user.json`, snippets в `assets/examples/kotlin` и `assets/examples/xml`, screenshots в `assets/screenshots`, metadata в `meta/samples.json` и доступные platform info files в `meta` + +#### Scenario: Задача агрегации завершена без user layer +- **WHEN** user structure отсутствует +- **THEN** Core output и общие artifacts SHALL быть созданы, а `structure-user.json` и `content/user` SHALL отсутствовать + +#### Scenario: Sample path разрешается от documentation root +- **WHEN** aggregator записывает Kotlin или XML sample metadata +- **THEN** `snippetPath` SHALL быть относительным к `.sdds/temp/docs` и начинаться с `assets/examples/kotlin/` или `assets/examples/xml/` + +#### Scenario: Входной файл отсутствует +- **WHEN** обязательный для выбранной платформы info-файл отсутствует +- **THEN** задача MUST завершиться ошибкой с точным путём отсутствующего файла + +#### Scenario: Core template отсутствует +- **WHEN** Core documentation artifact не содержит `structure.json` и markdown template +- **THEN** snippets, user layer и info-артефакты SHALL агрегироваться без создания фиктивной Core structure + +### Requirement: Portal build и публикация не входят в capability +Documentation capability SHALL NOT выполнять Docusaurus template generation, npm-команды, changelog synchronization, S3 upload или публикацию документации. + +#### Scenario: Выполняется агрегация документации +- **WHEN** запускается lifecycle-задача documentation capability +- **THEN** она SHALL завершаться созданием локального платформенного результата и SHALL NOT запускать portal build или network publication + +### Requirement: Core markdown насыщается платформенными examples +Documentation capability SHALL обрабатывать существующие Kotlin/XML sample directives в Core markdown и записывать результат в `.sdds/temp/docs/content/core`, сохраняя относительные логические пути страниц. + +#### Scenario: Kotlin sample существует +- **WHEN** Core markdown содержит Kotlin `@sample` directive, разрешимый по Core или локальным snippets +- **THEN** directive SHALL быть заменён содержимым соответствующего Kotlin example + +#### Scenario: XML sample существует +- **WHEN** Core markdown содержит XML `@sample` directive, разрешимый по Core или локальным snippets +- **THEN** directive SHALL быть заменён содержимым соответствующего XML example + +#### Scenario: Локальный example переопределяет Core example +- **WHEN** Core и локальный example разрешаются по одному пути +- **THEN** при насыщении SHALL использоваться локальный example + +#### Scenario: Обязательный sample не разрешается +- **WHEN** Core markdown содержит `@sample` directive, для которого отсутствует example +- **THEN** задача MUST завершиться ошибкой с путём markdown-файла и значением directive + +### Requirement: Core structure сопровождает насыщенный content +Documentation capability SHALL сохранять `structure.json` из Core documentation artifact как `structure-core.json` и SHALL сохранять Core content отдельно от user content. + +#### Scenario: Core structure валидно ссылается на templates +- **WHEN** все пути страниц из Core `structure.json` существуют в Core template +- **THEN** aggregator SHALL скопировать structure и создать соответствующие насыщенные файлы в `content/core` + +#### Scenario: Structure ссылается на отсутствующий markdown +- **WHEN** публичный path из Core `structure.json` отсутствует в Core template +- **THEN** задача MUST завершиться ошибкой с отсутствующим относительным путём + +#### Scenario: Template отсутствует в structure +- **WHEN** markdown-файл Core template не указан в Core `structure.json` +- **THEN** файл SHALL считаться непубличным и SHALL NOT попадать в `content/core` + +### Requirement: Style API насыщает markdown, а screenshots остаются внешним ресурсом +Documentation capability SHALL заменять существующие style-api directives платформенными данными, SHALL сохранять screenshot directives как ключи и SHALL копировать локальные screenshot assets для последующей публикации documentation service. + +#### Scenario: Markdown содержит screenshot directive +- **WHEN** markdown содержит `@screenshot` directive +- **THEN** aggregator SHALL сохранить directive без изменений, SHALL скопировать доступные PNG в `assets/screenshots` и SHALL NOT создавать локальную публичную ссылку + +#### Scenario: Compose style API существует +- **WHEN** markdown содержит `@style-api` directive и component info содержит style API +- **THEN** directive SHALL быть заменён таблицей параметров и примером выбора готового стиля + +### Requirement: User markdown насыщается независимо от Core +Documentation capability SHALL применять platform enrichment к публичному user markdown и SHALL сохранять его в `.sdds/temp/docs/content/user` без предварительного merge с Core content. + +#### Scenario: User sample существует +- **WHEN** публичный user markdown содержит разрешимый Kotlin или XML `@sample` directive +- **THEN** directive SHALL быть заменён соответствующим локальным example + +#### Scenario: User style API существует +- **WHEN** публичный Compose user markdown содержит `@style-api` и platform components info содержит соответствующий component +- **THEN** directive SHALL быть заменён таблицей параметров и примером выбора готового стиля + +#### Scenario: User screenshot key существует +- **WHEN** публичный user markdown содержит `@screenshot` +- **THEN** directive SHALL остаться неизменным, а доступный PNG SHALL находиться в общем `assets/screenshots` + +#### Scenario: Core и user имеют одинаковый logical path +- **WHEN** обе structures содержат один path +- **THEN** Android output SHALL содержать оба файла в разных namespaces и SHALL NOT объединять их содержимое + +### Requirement: User structure передаётся DS Builder CLI без merge +Documentation capability SHALL копировать валидный user structure как `.sdds/temp/docs/structure-user.json`, сохраняя navigation metadata и merge directives. + +#### Scenario: User structure валидно +- **WHEN** все публичные user paths разрешены и поддерживаемые merge modes валидны +- **THEN** aggregator SHALL записать `structure-user.json` без слияния с `structure-core.json` + +#### Scenario: Android aggregation получает merge metadata +- **WHEN** user page содержит `merge`, `hidden` или `subjects` +- **THEN** эти поля SHALL быть сохранены для последующей обработки DS Builder CLI + diff --git a/openspec/specs/core-documentation-templates/spec.md b/openspec/specs/core-documentation-templates/spec.md new file mode 100644 index 0000000000..485ec33824 --- /dev/null +++ b/openspec/specs/core-documentation-templates/spec.md @@ -0,0 +1,64 @@ +# core-documentation-templates Specification + +## Purpose +TBD - created by archiving change package-core-documentation-templates. Update Purpose after archive. +## Requirements +### Requirement: Core-документация имеет платформонезависимую структуру +Compose- и View-шаблоны Core-документации SHALL содержать `structure.json`, явно описывающий публичную навигацию и пути markdown-страниц в соответствии с ADR-0003. + +#### Scenario: Compose structure создаётся из существующей навигации +- **WHEN** поставляется Compose Core documentation template +- **THEN** его `structure.json` SHALL содержать quick start, группы темы, визуальных эффектов и компонентов с явными ссылками на существующие Compose markdown-файлы + +#### Scenario: View structure создаётся из существующей навигации +- **WHEN** поставляется View Core documentation template +- **THEN** его `structure.json` SHALL содержать quick start, группы темы и компонентов, а также страницу focus с явными ссылками на существующие View markdown-файлы + +#### Scenario: Docusaurus использует отдельную навигацию +- **WHEN** Core template одновременно содержит `sidebars.ts` и `structure.json` +- **THEN** `sidebars.ts` SHALL оставаться входом legacy Docusaurus, а `structure.json` SHALL быть входом DS Builder CLI без runtime-зависимости между форматами + +### Requirement: Публичные страницы перечисляются явно +`structure.json` SHALL явно перечислять каждый публикуемый markdown-файл и SHALL NOT использовать Docusaurus `autogenerated` semantics. + +#### Scenario: Markdown входит в публичную структуру +- **WHEN** markdown-страница должна быть опубликована как Core-документация +- **THEN** относительный путь страницы SHALL присутствовать в дереве `navigation` + +#### Scenario: Генерируемый changelog отсутствует в исходном шаблоне +- **WHEN** `CHANGELOG.md` создаётся Docusaurus-задачей после копирования Core template +- **THEN** Core `structure.json` SHALL NOT ссылаться на `CHANGELOG.md` + +### Requirement: Fixtures публикуют ограниченный docs-артефакт +`convention.core-fixtures` SHALL публиковать дополнительный docs JAR, содержащий Core template, извлечённые examples и sample metadata в `META-INF/sdds-docs`. + +#### Scenario: Упаковывается Core template +- **WHEN** собирается `docsJar` Compose- или View-fixtures +- **THEN** шаблонная часть JAR SHALL содержать только соответствующий `structure.json` и файлы `docs/**/*.md` + +#### Scenario: Упаковываются examples и metadata +- **WHEN** задача извлечения snippets завершена +- **THEN** JAR SHALL содержать Kotlin/XML examples и sample metadata в отдельных предсказуемых путях внутри `META-INF/sdds-docs` + +#### Scenario: Docusaurus-файлы исключены +- **WHEN** собирается `docsJar` +- **THEN** JAR SHALL NOT содержать `sidebars.ts`, `docusaurus.config.ts`, package manager files, Docusaurus source/runtime files, static portal assets или сгенерированный `CHANGELOG.md` + +#### Scenario: В артефакте нет произвольных файлов template directory +- **WHEN** в Docusaurus template появляется новый файл, не совпадающий с `structure.json` или `docs/**/*.md` +- **THEN** этот файл SHALL NOT автоматически попадать в docs JAR + +### Requirement: Gradle variant представляет templates +Producer и consumer Core documentation artifact SHALL использовать значение `templates` атрибута `com.sdds.docs.variant`. + +#### Scenario: Fixtures публикуют docs variant +- **WHEN** `convention.core-fixtures` объявляет consumable documentation configuration +- **THEN** configuration SHALL иметь `com.sdds.docs.variant=templates` + +#### Scenario: DS Builder Plugin резолвит docs variant +- **WHEN** documentation capability создаёт resolvable Core documentation configuration +- **THEN** configuration SHALL запрашивать `com.sdds.docs.variant=templates` + +#### Scenario: Совместимый snippets variant не публикуется +- **WHEN** собираются outgoing variants fixtures +- **THEN** отдельный вариант с `com.sdds.docs.variant=snippets` SHALL NOT публиковаться diff --git a/openspec/specs/ds-builder-gradle-dsl/spec.md b/openspec/specs/ds-builder-gradle-dsl/spec.md new file mode 100644 index 0000000000..0d29414eb7 --- /dev/null +++ b/openspec/specs/ds-builder-gradle-dsl/spec.md @@ -0,0 +1,73 @@ +# ds-builder-gradle-dsl Specification + +## Purpose +TBD - created by archiving change unify-ds-builder-plugin. Update Purpose after archive. +## Requirements +### Requirement: Единый публичный DS Builder DSL +Gradle-плагин с id `io.github.salute-developers.design-system-builder` SHALL регистрировать extension `dsBuilder` как единую точку конфигурации генерации темы, компонентов, документации и sandbox-адаптеров. + +#### Scenario: Проект конфигурирует одну возможность +- **WHEN** проект применяет DS Builder Gradle Plugin и конфигурирует только один из блоков `theme`, `components`, `documentation` или `sandbox` +- **THEN** плагин SHALL регистрировать и настраивать задачи этой возможности без требования сконфигурировать остальные блоки + +#### Scenario: Старый extension отсутствует +- **WHEN** проект применяет новую версию DS Builder Gradle Plugin +- **THEN** extension `themeBuilder` SHALL NOT регистрироваться и проект MUST использовать `dsBuilder` + +#### Scenario: Старый plugin id отсутствует +- **WHEN** repository modules мигрированы на DS Builder Gradle Plugin +- **THEN** они SHALL применять `io.github.salute-developers.design-system-builder` и SHALL NOT применять `io.github.salute-developers.theme-builder-plugin` + +### Requirement: Общая резолюция директории .sdds +`dsBuilder` SHALL предоставлять общее свойство `sddsDirectory` и автоматически находить `.sdds` для текущего или родительского проекта. + +#### Scenario: .sdds находится в текущем проекте +- **WHEN** `sddsDirectory` не задан явно и `${project.projectDir}/.sdds` существует +- **THEN** плагин SHALL использовать эту директорию + +#### Scenario: .sdds находится в родительском проекте +- **WHEN** `sddsDirectory` не задан явно, `.sdds` отсутствует в текущем проекте и `${project.parent.projectDir}/.sdds` существует +- **THEN** плагин SHALL использовать `.sdds` родительского проекта + +#### Scenario: Директория переопределена +- **WHEN** пользователь явно задаёт `sddsDirectory` +- **THEN** все включённые возможности SHALL разрешать стандартные DS Builder артефакты относительно указанной директории + +#### Scenario: Директория не найдена +- **WHEN** возможность требует локальные DS Builder артефакты, `sddsDirectory` не задана и `.sdds` не найдена в текущем или родительском проекте +- **THEN** конфигурация или выполнение соответствующей задачи MUST завершиться понятной ошибкой со списком проверенных путей + +### Requirement: Платформенные info-файлы имеют стандартные conventions +Плагин SHALL выводить стандартные `componentsInfoFile` и `themeInfoFile` из разрешённой `.sdds` директории и выбранной платформы. + +#### Scenario: Выбрана Compose-платформа +- **WHEN** documentation или sandbox capability настроена для Compose и пути не переопределены +- **THEN** `componentsInfoFile` SHALL указывать на `.sdds/config-info-compose.json`, а `themeInfoFile` SHALL указывать на `.sdds/theme-info-compose.json` + +#### Scenario: Выбрана View-платформа +- **WHEN** documentation или sandbox capability настроена для View и пути не переопределены +- **THEN** `componentsInfoFile` SHALL указывать на `.sdds/config-info-view-system.json`, а `themeInfoFile` SHALL указывать на `.sdds/theme-info-view-system.json` + +#### Scenario: Info-файл переопределён +- **WHEN** пользователь явно задаёт `componentsInfoFile` или `themeInfoFile` +- **THEN** соответствующая capability SHALL использовать явно заданный файл вместо стандартного пути + +### Requirement: Возможности плагина не зависят от внутренних convention-плагинов +DS Builder Gradle Plugin SHALL регистрировать собственные configurations, task types и platform-specific настройки documentation и sandbox capabilities без применения специализированных `convention.documentation-*` или `convention.integration-*`. + +#### Scenario: Плагин используется в token-модуле +- **WHEN** token-модуль применяет базовые Android/Compose conventions и DS Builder Gradle Plugin +- **THEN** documentation или sandbox capability SHALL работать без специализированного documentation/integration convention-плагина + +### Requirement: Theme и components наследуют общие generation settings +`dsBuilder` SHALL предоставлять общие `target`, `packageName`, `resourcePrefix`, `outputLocation` +и `dimensions`, применяемые как conventions к theme и components capabilities. + +#### Scenario: Общие dimensions используют Android resources +- **WHEN** пользователь включает `dimensions.fromResources` на уровне `dsBuilder` +- **THEN** и theme, и components generation SHALL получить `DimensionsConfig.fromResources = true` + +#### Scenario: Capability переопределяет общее значение +- **WHEN** theme или components задаёт собственное generation property +- **THEN** явно заданное capability value SHALL иметь приоритет над общей convention + diff --git a/openspec/specs/sandbox-adapter-generation/spec.md b/openspec/specs/sandbox-adapter-generation/spec.md new file mode 100644 index 0000000000..a296b5d0a7 --- /dev/null +++ b/openspec/specs/sandbox-adapter-generation/spec.md @@ -0,0 +1,71 @@ +# sandbox-adapter-generation Specification + +## Purpose +TBD - created by archiving change unify-ds-builder-plugin. Update Purpose after archive. +## Requirements +### Requirement: Sandbox capability генерирует платформенные адаптеры +Sandbox capability SHALL генерировать адаптеры темы и компонентов для sandbox/demo infrastructure из `componentsInfoFile`. + +#### Scenario: Compose sandbox +- **WHEN** модуль включает `sandbox { compose { ... } }` +- **THEN** плагин SHALL зарегистрировать Compose sandbox generation task с Compose target и выбранной схемой + +#### Scenario: View sandbox +- **WHEN** модуль включает `sandbox { view { ... } }` +- **THEN** плагин SHALL зарегистрировать View sandbox generation task с View target и выбранной схемой + +### Requirement: Sandbox inputs имеют типизированные conventions +Sandbox extension SHALL предоставлять типизированные Gradle properties для `componentsInfoFile`, `generatedPackageName`, `themeAlias`, `scheme` и Compose Multiplatform mode. + +#### Scenario: Стандартный components info +- **WHEN** `componentsInfoFile` не задан явно +- **THEN** sandbox capability SHALL использовать platform-specific файл из разрешённой `.sdds` + +#### Scenario: Package выводится из Android namespace +- **WHEN** `generatedPackageName` не задан явно и текущий Android-модуль имеет `namespace` +- **THEN** generator SHALL использовать Android namespace как пакет генерируемого кода + +#### Scenario: Package выводится без Android namespace +- **WHEN** `generatedPackageName` и Android namespace отсутствуют, но `componentsInfoFile` содержит `packageName` +- **THEN** generator SHALL вывести sandbox package из `componentsInfoFile.packageName` по документированному соглашению + +#### Scenario: Theme alias выводится из .sdds config +- **WHEN** `themeAlias` не задан явно +- **THEN** generator SHALL использовать alias первого базового tenant из `.sdds/config.json`, а при отсутствии alias — его name + +#### Scenario: Theme alias переопределён +- **WHEN** пользователь явно задаёт `themeAlias` +- **THEN** generator SHALL использовать явно заданное название темы + +### Requirement: Tenants не выбираются sandbox capability +Sandbox capability SHALL рассматривать tenants как вариации одной темы и SHALL NOT предоставлять параметр выбора tenant. + +#### Scenario: Config содержит несколько tenants +- **WHEN** `.sdds/config.json` содержит несколько tenant-вариаций +- **THEN** sandbox configuration SHALL разрешить одно название темы и SHALL NOT требовать выбора tenant + +### Requirement: Generated sources подключаются к сборке +Sandbox generator SHALL создавать код в Gradle build directory и подключать output directory к соответствующему Kotlin source set. + +#### Scenario: Android Compose или View module +- **WHEN** sandbox generation task создаёт код для Android-модуля +- **THEN** generated directory SHALL участвовать в компиляции без записи в `src/main` + +#### Scenario: Compose Multiplatform module +- **WHEN** Compose sandbox включает multiplatform mode +- **THEN** generated directory SHALL подключаться к согласованному common Kotlin source set + +### Requirement: Старые integration properties не используются +Sandbox capability SHALL NOT читать `integration.compose.*`, `integration.view.*` или `theme-alias` из Gradle properties. + +#### Scenario: Модуль мигрирован на sandbox DSL +- **WHEN** sandbox module применяет новый DS Builder Plugin +- **THEN** вся конфигурация generator SHALL поступать из typed extension properties и `.sdds` metadata + +### Requirement: Sandbox schema по умолчанию использует V2 +Sandbox capability SHALL использовать `SandboxScheme.V2`, когда schema явно не переопределена. + +#### Scenario: Schema не указана +- **WHEN** пользователь включает Compose или View sandbox без явного `scheme` +- **THEN** generation task SHALL использовать `SandboxScheme.V2` + diff --git a/openspec/specs/theme-builder-dsbuilder-source/spec.md b/openspec/specs/theme-builder-dsbuilder-source/spec.md index 5fbbc16b2b..d6e25c193a 100644 --- a/openspec/specs/theme-builder-dsbuilder-source/spec.md +++ b/openspec/specs/theme-builder-dsbuilder-source/spec.md @@ -1,30 +1,32 @@ -## ADDED Requirements +## Purpose +Define how DS Builder resolves local `.sdds` theme sources while preserving existing theme generation semantics. +## Requirements ### Requirement: Fallback source from .sdds config -`sdds-core/plugin_theme_builder` SHALL use `.sdds/config.json` as a fallback theme source when `themeBuilder` extension does not define `themeSource` or `themeSources`. +`sdds-core/plugin_theme_builder` SHALL use the `.sdds/config.json` resolved by `dsBuilder.sddsDirectory` as a fallback theme source when `dsBuilder.theme` does not define an explicit source. #### Scenario: Extension has no explicit theme source -- **WHEN** a project applies `plugin_theme_builder`, does not configure `themeSource` or `themeSources`, and contains `.sdds/config.json` -- **THEN** Theme Builder resolves theme sources from `.sdds/config.json` +- **WHEN** a project applies the DS Builder Gradle Plugin, enables `dsBuilder.theme`, does not configure an explicit source, and the resolved `.sdds/config.json` exists +- **THEN** the theme capability resolves theme sources from `.sdds/config.json` #### Scenario: Extension has explicit theme source -- **WHEN** a project configures `themeSource` or `themeSources` in `themeBuilder` -- **THEN** Theme Builder uses the explicit extension sources and does not replace them with `.sdds/config.json` sources +- **WHEN** a project configures an explicit source in `dsBuilder.theme` +- **THEN** the theme capability uses the explicit source and does not replace it with `.sdds/config.json` sources ### Requirement: DSBuilder tenants become legacy-compatible theme sources -Theme Builder SHALL convert each tenant from `.sdds/config.json` into a local theme source using `tenant.alias` when present, otherwise `tenant.name`. +The theme capability SHALL convert every tenant from `.sdds/config.json` into a local variation source of one theme using `tenant.alias` when present, otherwise `tenant.name`. #### Scenario: First tenant becomes internal default - **WHEN** `.sdds/config.json` contains at least one tenant -- **THEN** Theme Builder passes the first tenant to `GenerateThemeTask` with an empty tenant suffix so existing `Tenant.Default` behavior is preserved +- **THEN** the theme capability passes the first tenant to `GenerateThemeTask` with an empty tenant suffix so existing `Tenant.Default` behavior is preserved #### Scenario: Non-first tenants use public tenant names - **WHEN** `.sdds/config.json` contains tenants after the first tenant -- **THEN** Theme Builder passes each additional tenant to `GenerateThemeTask` with tenant suffix equal to `tenant.alias` when present, otherwise `tenant.name` +- **THEN** the theme capability passes each additional tenant to `GenerateThemeTask` with tenant suffix equal to `tenant.alias` when present, otherwise `tenant.name` #### Scenario: Base theme name comes from first tenant -- **WHEN** Theme Builder resolves sources from `.sdds/config.json` -- **THEN** Theme Builder uses the first tenant `alias` when present, otherwise `name`, as the base theme name +- **WHEN** the theme capability resolves sources from `.sdds/config.json` +- **THEN** it uses the first tenant `alias` when present, otherwise `name`, as the single base theme name exposed to documentation, sandbox and preview capabilities ### Requirement: Local tenant directories provide generation files Theme Builder SHALL read token files for local `.sdds` sources directly from each tenant directory instead of downloading and unpacking a theme zip archive. @@ -72,20 +74,21 @@ Theme Builder SHALL keep `GenerateThemeTask` and existing token/theme generators - **THEN** existing token generators, attribute generators, theme generators, and `Tenant.Default` semantics remain unchanged ### Requirement: Invalid .sdds config fails clearly -Theme Builder SHALL fail configuration with a clear error when `.sdds/config.json` is required as fallback but cannot provide usable theme sources. +The theme capability SHALL fail with a clear error when the resolved `.sdds/config.json` is required as fallback but cannot provide usable theme sources. #### Scenario: Missing config and no explicit source -- **WHEN** a project does not configure `themeSource` or `themeSources` and `.sdds/config.json` does not exist -- **THEN** Theme Builder fails with a message explaining that `themeSource(s)` or `.sdds/config.json` must be provided +- **WHEN** `dsBuilder.theme` does not configure an explicit source and the resolved `.sdds/config.json` does not exist +- **THEN** the theme capability fails with a message explaining that an explicit source or `.sdds/config.json` must be provided #### Scenario: Empty tenants - **WHEN** `.sdds/config.json` exists but contains no tenants -- **THEN** Theme Builder fails with a message explaining that `.sdds/config.json` must contain at least one tenant +- **THEN** the theme capability fails with a message explaining that `.sdds/config.json` must contain at least one tenant #### Scenario: Missing tenant files - **WHEN** a resolved local tenant directory does not contain required `meta.json` or `android/android_*.json` files -- **THEN** Theme Builder fails with a message that includes the missing file path +- **THEN** the theme capability fails with a message that includes the missing file path #### Scenario: Missing palette file -- **WHEN** Theme Builder resolves sources from `.sdds/config.json` and the resolved palette file does not exist -- **THEN** Theme Builder fails with a message that includes the missing palette file path +- **WHEN** the theme capability resolves sources from `.sdds/config.json` and the resolved palette file does not exist +- **THEN** the theme capability fails with a message that includes the missing palette file path + diff --git a/openspec/specs/user-documentation-layer/spec.md b/openspec/specs/user-documentation-layer/spec.md new file mode 100644 index 0000000000..ae7d2bda7b --- /dev/null +++ b/openspec/specs/user-documentation-layer/spec.md @@ -0,0 +1,69 @@ +# user-documentation-layer Specification + +## Purpose +TBD - created by archiving change aggregate-user-documentation-layer. Update Purpose after archive. +## Requirements +### Requirement: User documentation имеет явную структуру +Android documentation source SHALL принимать опциональный user layer из `override-docs/structure.json` и `override-docs/docs/**/*.md`, где structure явно перечисляет публичные логические пути страниц. + +#### Scenario: User layer отсутствует +- **WHEN** `override-docs/structure.json` отсутствует +- **THEN** Android aggregation SHALL завершиться без user structure и user content + +#### Scenario: User structure содержит самостоятельную страницу +- **WHEN** user structure ссылается на путь, отсутствующий в Core structure, и соответствующий markdown существует +- **THEN** страница SHALL считаться самостоятельной публичной user page + +#### Scenario: Markdown отсутствует в user structure +- **WHEN** user markdown не указан в `override-docs/structure.json` +- **THEN** файл SHALL считаться непубличным и SHALL NOT попадать в platform output + +#### Scenario: User structure ссылается на отсутствующий markdown +- **WHEN** публичный логический путь не разрешается в user markdown source +- **THEN** aggregation MUST завершиться ошибкой с логическим путём + +### Requirement: Legacy append сохраняет Docusaurus-совместимый source +User documentation SHALL поддерживать физический `+Name.md` как переходное представление логической страницы `Name.md` с `merge: append`. + +#### Scenario: Append дополняет Core page +- **WHEN** user structure содержит Core path с `merge: append` +- **THEN** source SHALL разрешаться из `+Name.md`, а user output SHALL сохранять логический путь `Name.md` без префикса + +#### Scenario: Legacy Docusaurus собирает append page +- **WHEN** выполняется существующая Docusaurus generation +- **THEN** `mergePlusPrefixedDocs` SHALL продолжать дописывать `+Name.md` к соответствующей Core page + +#### Scenario: Append source имеет неверное имя +- **WHEN** Core path объявлен с `merge: append`, но существует только обычный `Name.md` +- **THEN** validation MUST завершиться ошибкой и указать ожидаемый `+Name.md` + +#### Scenario: Самостоятельная page использует plus prefix +- **WHEN** логический path отсутствует в Core structure, но source назван `+Name.md` +- **THEN** validation MUST завершиться ошибкой, потому что plus prefix допустим только для append к Core page + +### Requirement: User merge contract однозначен для обоих pipelines +User structure SHALL явно отличать append и replace существующей Core page, а неподдерживаемый legacy merge mode SHALL отклоняться до насыщения. + +#### Scenario: User page заменяет Core page +- **WHEN** user structure содержит Core path с `merge: replace` и обычный `Name.md` +- **THEN** Android aggregation SHALL сохранить отдельный user content, а legacy Docusaurus SHALL продолжить использовать overlay replacement + +#### Scenario: Пересекающийся path не имеет merge +- **WHEN** user path существует в Core structure, но merge mode не задан +- **THEN** validation MUST завершиться ошибкой и потребовать явный `append` или `replace` + +#### Scenario: Prepend запрошен при включённой legacy совместимости +- **WHEN** user structure содержит `merge: prepend` +- **THEN** validation MUST завершиться понятной ошибкой, поскольку legacy Docusaurus не поддерживает prepend + +### Requirement: Существующие user overrides получают декларативную навигацию +Существующие user markdown в token docs modules SHALL быть описаны user structure без изменения их Docusaurus merge semantics. + +#### Scenario: Homeds overrides валидируются +- **WHEN** проверяется `plasma.homeds.compose/docs/override-docs` +- **THEN** все текущие append и standalone markdown SHALL быть перечислены в user structure с соответствующими логическими paths + +#### Scenario: Sbcom overrides валидируются +- **WHEN** проверяется `sdds-sbcom-compose/docs/override-docs` +- **THEN** текущий `+IndicationUsage.md` SHALL быть представлен логическим Core path с `merge: append` + diff --git a/playground/theme-builder/build.gradle.kts b/playground/theme-builder/build.gradle.kts index 9c14c37524..2cfb74510b 100644 --- a/playground/theme-builder/build.gradle.kts +++ b/playground/theme-builder/build.gradle.kts @@ -6,7 +6,7 @@ import com.sdds.plugin.themebuilder.ThemeBuilderMode plugins { id("convention.android-lib") id("convention.compose") - id(libs.plugins.themebuilder.get().pluginId) + id(libs.plugins.dsbuilder.get().pluginId) } android { @@ -14,22 +14,8 @@ android { resourcePrefix = "thmbldr" } -themeBuilder { - themeSources(baseAlias = "SddsServ") { - defaultSourceFromUrl( - name = "sdds_serv", - url = "file://${projectDir.path}/json/latest.zip", - ) - sourceFromUrl( - name = "sdds_serv", - url = "file://${projectDir.path}/json/latest_gold.zip", - tenant = "Gold", - ) - } - componentSource { - url("file://${projectDir.path}/json/test_components.zip") - name("sdds_serv") - } +dsBuilder { + autoGenerate.set(false) view { themeParents { materialComponentsTheme() @@ -37,10 +23,8 @@ themeBuilder { setupShapeAppearance(sddsShape()) } compose() - ktPackage("com.sdds.playground.themebuilder") - mode(ThemeBuilderMode.THEME) - outputLocation(OutputLocation.BUILD) - autoGenerate(false) + packageName.set("com.sdds.playground.themebuilder") + outputLocation.set(OutputLocation.BUILD) dimensions { multiplier(2f) breakPoints { @@ -49,6 +33,23 @@ themeBuilder { } fromResources(false) } + theme { + sources(baseAlias = "SddsServ") { + defaultSourceFromUrl( + name = "sdds_serv", + url = "file://${projectDir.path}/json/latest.zip", + ) + sourceFromUrl( + name = "sdds_serv", + url = "file://${projectDir.path}/json/latest_gold.zip", + tenant = "Gold", + ) + } + mode.set(ThemeBuilderMode.THEME) + } + components { + source("file://${projectDir.path}/json/test_components.zip") + } } dependencies { diff --git a/sdds-core/plugin_theme_builder/README.MD b/sdds-core/plugin_theme_builder/README.MD index 03759d0a00..14f8c805fb 100644 --- a/sdds-core/plugin_theme_builder/README.MD +++ b/sdds-core/plugin_theme_builder/README.MD @@ -1,202 +1,120 @@ -## Плагин ThemeBuilder +# Design System Builder Gradle Plugin -Плагин генерирует тему и токены на основе метаданных. Источником данных служит zip архив, который содержит: -- meta.json файл с мета информацией о токенах. -- папку android, содержащую файлы, начинающиеся с префикса "android_". Файлы содержат значения токенов для соответствующих ключей из meta.json. +Плагин с id `io.github.salute-developers.design-system-builder` объединяет четыре независимые +возможности: генерацию темы, генерацию компонентов, подготовку Android-документации и генерацию +адаптеров для sandbox. -## Установка -
-Добавить зависимость в gradle - -```groovy +```toml [plugins] -themebuilder = { id = "io.github.salute-developers.theme-builder-plugin" } +dsbuilder = { id = "io.github.salute-developers.design-system-builder", version = "" } ``` -
- -
- -Подключить плагин в build.gradle модуля ```kotlin plugins { - id(libs.plugins.themebuilder.get().pluginId) + alias(libs.plugins.dsbuilder) } ``` -
-
-Далее в `build.gradle` необходимо сконфигурировать плагин: +Единственная публичная точка конфигурации — `dsBuilder`. Старые plugin id и `themeBuilder` DSL +не поддерживаются. -
-Пример конфигурации для view и compose +## Общая директория `.sdds` -```kotlin -plugins { - ... - id(libs.plugins.themebuilder.get().pluginId) -} +`sddsDirectory` по умолчанию ищется сначала в текущем project, затем в его непосредственном parent. +Путь можно переопределить: -android { - namespace = "com.example.app.themebuilder" - resourcePrefix = "thmbldr" -} - -themeBuilder { - themeSource(name = "stylesSalute", version = "0.1.0") - view(parentThemeName = "Sdds.Theme") - compose() - ktPackage("com.example.app.themebuilder.tokens") +```kotlin +dsBuilder { + sddsDirectory.set(layout.projectDirectory.dir("design-system")) } ``` -
-
-Минимальная настройка должна содержать источник темы `themeSource` или локальный `.sdds/config.json` и один из целевых фреймворков view | compose. -## Конфигурация +Стандартные файлы: -#### `themeSource` -Источник для скачивания архива с темой. Возможны два варианта: по названию и номеру версии (предпочтительный способ) и по url. Если указать только название, будет скачана latest версия архива с мета данными темы. +| Назначение | Compose | View | +|---|---|---| +| Components info | `config-info-compose.json` | `config-info-view-system.json` | +| Theme info | `theme-info-compose.json` | `theme-info-view-system.json` | -
-Примеры настройки themeSource +Если источник темы не задан явно, `theme` читает `.sdds/config.json`. Первый tenant становится +внутренним `Tenant.Default`, остальные сохраняются как вариации одной темы. Alias темы берётся из +`tenant.alias`, с fallback на `tenant.name`. + +## Theme и components ```kotlin -themeSource(name = "stylesSalute") // version = "latest" -``` -```kotlin -themeSource(name = "stylesSalute", version = "0.1.0") -``` -```kotlin -themeSource(url = "https://example.com/themes/theme.zip") -``` -
- -#### `.sdds/config.json` -Если `themeSource` или `themeSources` не заданы явно, плагин использует `.sdds/config.json` как fallback-источник темы. Такой формат подходит для модулей, где DSBuilder уже выгрузил theme data в локальную директорию `.sdds/`. - -Плагин читает массив `tenants` и для каждого tenant использует `alias`, если он задан, иначе `name`. Если у tenant есть `directoryPath`, файлы темы читаются из этой директории относительно модуля. Если `directoryPath` отсутствует, используется путь `.sdds/`. - -Каждая tenant-директория должна содержать: -- `meta.json` -- `android/android_color.json` -- `android/android_gradient.json` -- `android/android_typography.json` -- `android/android_fontFamily.json` -- `android/android_shape.json` -- `android/android_shadow.json` -- `android/android_spacing.json` - -Первый tenant из `.sdds/config.json` используется как внутренняя база `Tenant.Default`: в `GenerateThemeTask` он передается с пустым tenant suffix. Остальные tenants передаются с suffix `alias ?: name`. - -Локальный `.sdds` source заменяет загрузку theme zip и palette: для него не создаются `fetchTheme*`, `unpackThemeFiles*` и `fetchPalette` tasks. Палитра читается из `palettePath` в `.sdds/config.json`, а если поле не задано, из `.sdds/tenants/palette.json`. Для явно заданных `themeSource` или `themeSources` палитра по-прежнему скачивается из `paletteUrl`. - -
-Пример .sdds/config.json - -```json -{ - "tenants": [ - { - "name": "sdds_default", - "alias": "Default", - "directoryPath": ".sdds/sdds_default" - }, - { - "name": "sdds_partner", - "directoryPath": ".sdds/sdds_partner" +import com.sdds.plugin.themebuilder.OutputLocation.SRC +import com.sdds.plugin.themebuilder.ThemeBuilderMode.THEME + +dsBuilder { + autoGenerate.set(false) + compose() + packageName.set("com.example.designsystem") + resourcePrefix.set("example") + outputLocation.set(SRC) + dimensions { + fromResources(true) + } + theme { + source(name = "stylesSalute", version = "latest", alias = "Salute") + mode.set(THEME) + } + components { + source(name = "componentsSalute", version = "latest", alias = "Salute") + componentsMetaStyleClass.set(true) } - ] } ``` -
-#### `view` (optional) -Опциональная настройка генерации тем для view фреймворка. Можно указать список наследуемых тем. -Если не указать, то сгенерированная тема для view не будет иметь parent'a и будет представлена в двух директориях res/values и res/values-night. +Для View используйте `view { themeParents { ... } }`. Для Compose Multiplatform — +`compose(multiplatform = true)`. URL-источники и несколько tenant-вариаций поддерживаются через +`source(url = ..., name = ...)` и `sources(baseAlias = ...)`. Общие `target`, `packageName`, +`resourcePrefix`, `outputLocation` и `dimensions` задаются на уровне `dsBuilder`; одноимённые +capability-настройки остаются доступными как точечные overrides. -
-Примеры настройки view +## Documentation ```kotlin -view { - themeParents { - materialComponentsTheme() // Наследуемся от Theme.MaterialComponents и генерируем единственную тему c темными токенами - materialComponentsTheme("DayNight") // Наследуемся от Theme.MaterialComponents.DayNight - appCompatTheme("Light.NoActionBar") // Наследуемся от Theme.AppCompat.Light.NoActionBar и генерируем единственную тему cо светлыми токенами - customTheme("Theme.App", ViewThemeType.DARK_LIGHT) // Наследумся от Theme.App и генерируем тему для двух режимов - customTheme("Theme.Custom", ViewThemeType.DARK) // Наследумся от Theme.Custom и генерируем единственную тему c темными токенами +dsBuilder { + documentation { + compose() // либо view() } } ``` -```kotlin -view() // Тема не будет иметь parent'a -``` -
-#### `compose` (optional) -Опциональная настройка генерации темы для Compose. +Задача `documentationAggregate` создаёт platform enrichment в `.sdds/temp/docs`: -
-Пример настройки compose - -```kotlin -compose() -``` -
- -#### `ktPackage` (optional) -Для генерации темы любого фреймворка необходимо указать ktPackage - пакет для генерируемых kotlin-файлов. Итоговый пакет будет состоять из указанного ktPackage + ".compose" или ".vs". - -
-Пример настройки пакета - -```kotlin -ktPackage("com.example.app.themebuilder.tokens") -``` -
- -#### `resourcesPrefix` (optional) -Префикс для названий ресурсов. Можно не указывать, если он уже указан в android конфигурации (см. пример выше). - -
-Пример настройки resourcesPrefix - -```kotlin -resourcesPrefix(prefix = "ds") +```text +assets/examples/kotlin/ +assets/examples/xml/ +meta/samples.json +meta/components-info.json +meta/theme-info.json ``` -
-#### `mode` (optional) -Режим работы плагина - генерируются только токены либо тема с токенами. По умолчанию генерируются только токены. +Core snippet artifacts подключаются в configuration `sddsCoreDocumentation`; их `meta.json` +объединяется с локальной metadata в `meta/samples.json`. Поле `snippetPath` хранится относительно +корня `.sdds/temp/docs`, например `assets/examples/kotlin/com/example/Sample.kt`. Плагин не запускает +Docusaurus, npm, публикацию или сетевые portal-задачи и не создаёт `manifest.json`/`docs.json`: +это ответственность `dsbuilder docs generate`. -
-Пример настройки mode +## Sandbox ```kotlin -mode(mode = ThemeBuilderMode.TOKENS_ONLY) // сгенерируются только токены -``` -```kotlin -mode(mode = ThemeBuilderMode.THEME) // сгенерируются токены и темы -``` -
- -#### `paletteUrl` (optional) -Ссылка для скачивания палитры цветов. - -
-Пример настройки paletteUrl - -```kotlin -paletteUrl(url = "https://example.com/themes/color-palette.json") +dsBuilder { + sandbox { + compose { + multiplatform.set(false) + // componentsInfoFile, generatedPackageName и themeAlias имеют conventions + } + } +} ``` -
- -## Использование -Необходимо запустить сборку модуля, куда был подключен плагин: - `./gradlew :your_module:assemble` +Для View используется `sandbox { view { ... } }`. `componentsInfoFile` выводится из `.sdds`, +package — из Android namespace (либо `componentsInfo.packageName + ".sandbox"`), theme alias — +из первого базового tenant. Scheme по умолчанию — `SandboxScheme.V2`. Все значения можно явно +переопределить. -Плагин скачает архив с темой из источника, указанного в `themeSource` и сгенерирует токены/темы под указанные фреймворки. По пути `your_module/build/generated/` должны появиться папки: -- `your_module/build/generated/theme-builder` - Kotlin-файлы темы. Может содержать токены и тему для compose, а также токены градиентов для view. -- `your_module/build/generated/theme-builder-res` - ресурсы темы. Содержит токены и темы для view, а также ресурсы шрифтов для compose. +Generated sandbox sources записываются в `build/generated/sdds/sandbox` и автоматически +подключаются к Android `main` или KMP `commonMain`. diff --git a/sdds-core/plugin_theme_builder/build.gradle.kts b/sdds-core/plugin_theme_builder/build.gradle.kts index c3b7bb5010..1ae81a78c1 100644 --- a/sdds-core/plugin_theme_builder/build.gradle.kts +++ b/sdds-core/plugin_theme_builder/build.gradle.kts @@ -31,12 +31,12 @@ gradlePlugin { website.set(findPropertyOrDefault("nexus.websiteUrl", "").toString()) vcsUrl.set(findPropertyOrDefault("nexus.gitUrl", "").toString()) plugins { - create("themeBuilderPlugin") { - id = "io.github.salute-developers.theme-builder-plugin" - displayName = "Theme Builder Plugin" - description = "Plugin automatically generates design tokens based on configuration provided by ThemeBuilder" + create("dsBuilderPlugin") { + id = "io.github.salute-developers.design-system-builder" + displayName = "Design System Builder Plugin" + description = "Unified plugin for design system theme, components, documentation and sandbox generation" tags.add("designSystem") - implementationClass = "com.sdds.plugin.themebuilder.ThemeBuilderPlugin" + implementationClass = "com.sdds.plugin.themebuilder.DsBuilderPlugin" } } } @@ -50,7 +50,10 @@ publishing { dependencies { implementation(libs.base.kotlin.serialization.json) implementation(libs.base.gradle.android) + implementation(libs.base.gradle.kotlin) implementation(libs.base.kotlin.poet) + implementation("com.google.code.gson:gson:2.11.0") + compileOnly(libs.base.kotlin.compiler.embeddable) testImplementation(libs.base.test.unit.jUnit) testImplementation(libs.base.test.unit.mockk) } diff --git a/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/DsBuilderExtension.kt b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/DsBuilderExtension.kt new file mode 100644 index 0000000000..fef7962899 --- /dev/null +++ b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/DsBuilderExtension.kt @@ -0,0 +1,439 @@ +@file:Suppress("CompanionObjectInEnd", "UnnecessaryAbstractClass") + +package com.sdds.plugin.themebuilder + +import com.sdds.plugin.themebuilder.internal.ThemeBuilderTarget +import com.sdds.plugin.themebuilder.sandbox.SandboxScheme +import org.gradle.api.Action +import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.api.provider.SetProperty +import org.gradle.kotlin.dsl.newInstance +import javax.inject.Inject + +/** + * Public configuration entry point of the Design System Builder plugin. + * + * [sddsDirectory] defaults to `.sdds` in the current project and then to + * `.sdds` in its immediate parent project. An explicit value always wins. + */ +abstract class DsBuilderExtension @Inject constructor( + objects: ObjectFactory, +) { + + /** Shared directory containing DS Builder metadata. */ + abstract val sddsDirectory: DirectoryProperty + + /** Standard DS Builder configuration at `.sdds/config.json`. */ + abstract val configFile: RegularFileProperty + + /** Kotlin package shared by theme and component generation. */ + abstract val packageName: Property + + /** Android resource prefix shared by theme and component generation. */ + abstract val resourcePrefix: Property + + /** Android UI targets shared by theme and component generation. */ + abstract val targets: SetProperty + + /** Parents shared by generated View themes and components. */ + abstract val viewThemeParents: SetProperty + + /** Shape mappings shared by generated View themes and components. */ + abstract val viewShapeAppearance: SetProperty + + /** Output location shared by theme, component and sandbox generation. */ + abstract val outputLocation: Property + + /** Whether enabled capability tasks are attached to Android `preBuild`. */ + abstract val autoGenerate: Property + + /** Dimension settings shared by theme and component generation. */ + abstract val dimensions: Property + + /** Whether shared Compose generation targets a multiplatform source set. */ + abstract val multiplatform: Property + + /** Theme generation settings. */ + val theme: ThemeCapability = objects.newInstance() + + /** Component generation settings. */ + val components: ComponentsCapability = objects.newInstance() + + /** Android documentation aggregation settings. */ + val documentation: DocumentationCapability = objects.newInstance() + + /** Sandbox adapter generation settings. */ + val sandbox: SandboxCapability = objects.newInstance() + + init { + targets.convention(emptySet()) + viewThemeParents.convention(emptySet()) + viewShapeAppearance.convention(emptySet()) + outputLocation.convention(OutputLocation.BUILD) + autoGenerate.convention(true) + dimensions.convention(DimensionsConfig()) + multiplatform.convention(false) + + listOf(theme, components).forEach { capability -> + capability.packageName.convention(packageName) + capability.resourcesPrefix.convention(resourcePrefix) + capability.targets.convention(targets) + capability.viewThemeParents.convention(viewThemeParents) + capability.viewShapeAppearance.convention(viewShapeAppearance) + capability.outputLocation.convention(outputLocation) + capability.dimensions.convention(dimensions) + capability.multiplatform.convention(multiplatform) + } + listOf(theme, components, documentation, sandbox).forEach { capability -> + capability.autoGenerate.convention(autoGenerate) + } + sandbox.outputLocation.convention(outputLocation) + } + + /** Enables Compose generation for both theme and components. */ + fun compose(multiplatform: Boolean = false) { + targets.add(DsBuilderPlatform.COMPOSE) + this.multiplatform.set(multiplatform) + } + + /** Enables View generation for both theme and components. */ + fun view(action: ViewConfigBuilder.() -> Unit = {}) { + val builder = ViewConfigBuilder().apply(action) + targets.add(DsBuilderPlatform.VIEW) + viewThemeParents.set(builder.themeParents) + viewShapeAppearance.set(builder.shapeAppearanceConfig) + } + + /** Configures dimensions for both theme and components. */ + fun dimensions(action: DimensionsConfigBuilder.() -> Unit) { + dimensions.set(DimensionsConfigBuilder().apply(action).build()) + } + + /** Enables and configures theme generation. */ + fun theme(action: Action) { + theme.enabled.set(true) + action.execute(theme) + } + + /** Enables and configures component generation. */ + fun components(action: Action) { + components.enabled.set(true) + action.execute(components) + } + + /** Enables and configures documentation aggregation. */ + fun documentation(action: Action) { + documentation.enabled.set(true) + action.execute(documentation) + documentation.compose?.applyInfoConventions(DsBuilderPlatform.COMPOSE) + documentation.view?.applyInfoConventions(DsBuilderPlatform.VIEW) + } + + /** Enables and configures sandbox adapter generation. */ + fun sandbox(action: Action) { + sandbox.enabled.set(true) + action.execute(sandbox) + sandbox.compose?.applyComponentsInfoConvention(DsBuilderPlatform.COMPOSE) + sandbox.view?.applyComponentsInfoConvention(DsBuilderPlatform.VIEW) + } + + internal companion object { + fun Project.dsBuilderExt(): DsBuilderExtension { + val extension = extensions.create("dsBuilder", DsBuilderExtension::class.java) + extension.sddsDirectory.convention( + layout.dir( + providers.provider { + SddsDirectoryResolver(projectDir, parent?.projectDir).resolveOrNull() + }, + ), + ) + extension.configFile.convention(extension.sddsDirectory.file("config.json")) + extension.documentation.outputDirectory.convention(extension.sddsDirectory.dir("temp/docs")) + extension.documentation.userDocumentationRoot.convention( + project.layout.projectDirectory.dir("override-docs"), + ) + return extension + } + } + + private fun DocumentationPlatform.applyInfoConventions(platform: DsBuilderPlatform) { + componentsInfoFile.convention(sddsDirectory.file(platform.componentsInfoName)) + themeInfoFile.convention(sddsDirectory.file(platform.themeInfoName)) + } + + private fun SandboxPlatform.applyComponentsInfoConvention(platform: DsBuilderPlatform) { + componentsInfoFile.convention(sddsDirectory.file(platform.componentsInfoName)) + } +} + +/** Settings shared by a lazily activated capability. */ +abstract class DsBuilderCapability { + /** Whether this capability was activated by its DSL block. */ + abstract val enabled: Property + + /** Whether this capability is attached to Android `preBuild`. */ + abstract val autoGenerate: Property + + init { + enabled.convention(false) + autoGenerate.convention(true) + } +} + +/** + * Theme generation settings. + * + * Existing theme generator options will be exposed by this composed model as + * the legacy task registration is adapted to the new DSL. + */ +abstract class ThemeCapability : GenerationCapability() { + /** Explicit theme sources. `.sdds/config.json` is used when absent. */ + internal abstract val sources: Property + + /** Palette URL used by remote theme sources. */ + abstract val paletteUrl: Property + + /** Theme generator mode. */ + abstract val mode: Property + + /** Default typography strategy used by generated themes. */ + abstract val defaultTypography: Property + + /** Whether disabled tokens are omitted. */ + abstract val ignoreDisabledTokens: Property + + /** Whether Compose font tokens use the platform default family. */ + abstract val useDefaultFonts: Property + + init { + paletteUrl.convention(DEFAULT_PALETTE_URL) + mode.convention(ThemeBuilderMode.TOKENS_ONLY) + defaultTypography.convention(DefaultThemeTypography.DYNAMIC) + ignoreDisabledTokens.convention(false) + useDefaultFonts.convention(false) + } + + /** Configures one remote theme source. */ + fun source(name: String, version: String = ThemeSourceBuilder.VERSION_LATEST, alias: String = name) { + sources.set( + ThemeBuilderSources( + baseAlias = alias, + sources = listOf(ThemeBuilderSource.withNameAndVersion(name, version, alias)), + ), + ) + } + + /** Configures one remote theme source by URL. */ + fun source(url: String, name: String) { + sources.set( + ThemeBuilderSources( + baseAlias = name, + sources = listOf(ThemeBuilderSource.withUrl(url, name)), + ), + ) + } + + /** Configures all tenant variations of one theme. */ + fun sources(baseAlias: String = "", action: ThemeSourcesBuilder.() -> Unit) { + val builder = ThemeSourcesBuilder(baseAlias).apply(action) + require(builder.defaultSourceWasDefined) { + "Default source must be defined when using multitenant mode" + } + sources.set(ThemeBuilderSources(baseAlias, builder.sources)) + } + + private companion object { + const val DEFAULT_PALETTE_URL = + "https://raw.githubusercontent.com/salute-developers/plasma/dev/packages/plasma-colors/palette/general.json" + } +} + +/** + * Component generation settings. + * + * Existing component generator options will be exposed by this composed model + * as the legacy task registration is adapted to the new DSL. + */ +abstract class ComponentsCapability : GenerationCapability() { + /** Remote component configuration source. */ + internal abstract val source: Property + + /** Whether generated Compose metadata includes style classes. */ + abstract val componentsMetaStyleClass: Property + + init { + componentsMetaStyleClass.convention(false) + } + + /** Configures a component source by name and version. */ + fun source(name: String, version: String = ThemeSourceBuilder.VERSION_LATEST, alias: String = name) { + source.set(ThemeBuilderSource.withNameAndVersion(name, version, alias)) + } + + /** Configures a component source by URL. */ + fun source(url: String) { + source.set(ThemeBuilderSource.withUrl(url)) + } +} + +/** Typed output and platform options shared by theme and component generation. */ +abstract class GenerationCapability : DsBuilderCapability() { + /** Kotlin package of generated sources. */ + abstract val packageName: Property + + /** Prefix of generated Android resources. */ + abstract val resourcesPrefix: Property + + /** Selected Android UI targets. */ + abstract val targets: SetProperty + + /** Parents used for generated View themes. */ + abstract val viewThemeParents: SetProperty + + /** Shape mappings used for generated View themes. */ + abstract val viewShapeAppearance: SetProperty + + /** Location of generated source and resource files. */ + abstract val outputLocation: Property + + /** Dimension generation settings. */ + abstract val dimensions: Property + + /** Whether output targets Compose Multiplatform. */ + abstract val multiplatform: Property + + init { + targets.convention(emptySet()) + viewThemeParents.convention(emptySet()) + viewShapeAppearance.convention(emptySet()) + outputLocation.convention(OutputLocation.BUILD) + dimensions.convention(DimensionsConfig()) + multiplatform.convention(false) + } + + /** Enables Compose generation. */ + fun compose(multiplatform: Boolean = false) { + targets.add(DsBuilderPlatform.COMPOSE) + this.multiplatform.set(multiplatform) + } + + /** Enables View generation with optional theme configuration. */ + fun view(action: ViewConfigBuilder.() -> Unit = {}) { + val builder = ViewConfigBuilder().apply(action) + targets.add(DsBuilderPlatform.VIEW) + viewThemeParents.set(builder.themeParents) + viewShapeAppearance.set(builder.shapeAppearanceConfig) + } + + /** Configures generated dimension scaling and breakpoints. */ + fun dimensions(action: DimensionsConfigBuilder.() -> Unit) { + dimensions.set(DimensionsConfigBuilder().apply(action).build()) + } + + internal fun target(): ThemeBuilderTarget? = when (targets.getOrElse(emptySet())) { + setOf(DsBuilderPlatform.COMPOSE) -> ThemeBuilderTarget.COMPOSE + setOf(DsBuilderPlatform.VIEW) -> ThemeBuilderTarget.VIEW_SYSTEM + setOf(DsBuilderPlatform.COMPOSE, DsBuilderPlatform.VIEW) -> ThemeBuilderTarget.ALL + else -> null + } +} + +/** Platform-specific documentation aggregation settings. */ +abstract class DocumentationCapability @Inject constructor( + private val objects: ObjectFactory, +) : DsBuilderCapability() { + /** Platform enrichment root, conventionally `.sdds/temp/docs`. */ + abstract val outputDirectory: DirectoryProperty + + /** Optional design-system documentation root, conventionally `override-docs`. */ + abstract val userDocumentationRoot: DirectoryProperty + + /** Compose documentation settings, or `null` when Compose is not enabled. */ + var compose: DocumentationPlatform? = null + private set + + /** View documentation settings, or `null` when View is not enabled. */ + var view: DocumentationPlatform? = null + private set + + /** Enables Compose documentation aggregation. */ + fun compose(action: Action = Action {}) { + compose = objects.newInstance().also(action::execute) + } + + /** Enables View documentation aggregation. */ + fun view(action: Action = Action {}) { + view = objects.newInstance().also(action::execute) + } +} + +/** Inputs of documentation aggregation for one Android UI platform. */ +abstract class DocumentationPlatform { + /** Override for the platform components info JSON. */ + abstract val componentsInfoFile: RegularFileProperty + + /** Override for the platform theme info JSON. */ + abstract val themeInfoFile: RegularFileProperty +} + +/** Sandbox adapter generation settings. */ +abstract class SandboxCapability @Inject constructor( + private val objects: ObjectFactory, +) : DsBuilderCapability() { + /** Location of generated sandbox adapter sources. */ + abstract val outputLocation: Property + + /** Compose adapter settings, or `null` when Compose is not enabled. */ + var compose: ComposeSandboxPlatform? = null + private set + + /** View adapter settings, or `null` when View is not enabled. */ + var view: ViewSandboxPlatform? = null + private set + + /** Enables Compose sandbox adapter generation. */ + fun compose(action: Action) { + compose = objects.newInstance().also(action::execute) + } + + /** Enables View sandbox adapter generation. */ + fun view(action: Action) { + view = objects.newInstance().also(action::execute) + } +} + +/** Typed sandbox inputs shared by Compose and View generators. */ +abstract class SandboxPlatform { + /** Override for the platform components info JSON. */ + abstract val componentsInfoFile: RegularFileProperty + + /** Package of generated sandbox sources. */ + abstract val generatedPackageName: Property + + /** Public name of the generated design-system theme. */ + abstract val themeAlias: Property + + /** Generator API scheme. */ + abstract val scheme: Property + + init { + scheme.convention(SandboxScheme.V2) + } +} + +/** Compose sandbox adapter settings. */ +abstract class ComposeSandboxPlatform : SandboxPlatform() { + /** Whether generated sources target a Compose Multiplatform source set. */ + abstract val multiplatform: Property + + init { + multiplatform.convention(false) + } +} + +/** View sandbox adapter settings. */ +abstract class ViewSandboxPlatform : SandboxPlatform() diff --git a/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/DsBuilderPlugin.kt b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/DsBuilderPlugin.kt new file mode 100644 index 0000000000..186fde611e --- /dev/null +++ b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/DsBuilderPlugin.kt @@ -0,0 +1,244 @@ +package com.sdds.plugin.themebuilder + +import com.android.build.gradle.BaseExtension +import com.google.gson.Gson +import com.sdds.plugin.themebuilder.DsBuilderExtension.Companion.dsBuilderExt +import com.sdds.plugin.themebuilder.documentation.DocumentationAggregateTask +import com.sdds.plugin.themebuilder.documentation.ExtractCodeSnippetsTask +import com.sdds.plugin.themebuilder.sandbox.Config +import com.sdds.plugin.themebuilder.sandbox.GenerateSandboxAdaptersTask +import com.sdds.plugin.themebuilder.sandbox.SandboxTarget +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.attributes.Attribute +import org.gradle.api.attributes.Category +import org.gradle.api.attributes.Usage +import org.gradle.api.file.Directory +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.TaskProvider +import org.gradle.kotlin.dsl.findByType +import org.gradle.kotlin.dsl.register +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension + +/** + * Entry point for theme, component, documentation and sandbox generation. + * + * Capabilities are enabled independently by configuring their corresponding + * blocks in the [DsBuilderExtension]. + */ +class DsBuilderPlugin : Plugin { + + override fun apply(project: Project) { + val extension = project.dsBuilderExt() + ThemeBuilderPlugin().configure( + project = project, + themeExtension = { + extension.theme + .takeIf { it.enabled.get() } + ?.toLegacyExtension(extension) + }, + componentsExtension = { + extension.components + .takeIf { it.enabled.get() } + ?.toLegacyExtension() + }, + ) + project.configureDocumentation(extension) + project.configureSandbox(extension) + } +} + +private fun Project.configureSandbox(extension: DsBuilderExtension) { + afterEvaluate { + val sandbox = extension.sandbox.takeIf { it.enabled.get() } ?: return@afterEvaluate + sandbox.compose?.let { platform -> + configureSandboxConventions(extension, platform) + val output = sandboxOutputDirectory(sandbox.outputLocation.get(), platform.multiplatform.get()) + val task = registerSandboxTask( + name = "generateComposeSandbox", + platform = platform, + target = SandboxTarget.COMPOSE, + output = output, + multiplatform = platform.multiplatform.get(), + ) + attachToPreBuild(task, sandbox.autoGenerate.get()) + if (platform.multiplatform.get()) { + extensions.findByType() + ?.sourceSets + ?.findByName("commonMain") + ?.kotlin + ?.srcDir(task.flatMap { it.outputDirectory }) + } + } + sandbox.view?.let { platform -> + configureSandboxConventions(extension, platform) + val output = sandboxOutputDirectory(sandbox.outputLocation.get(), multiplatform = false) + val task = registerSandboxTask( + name = "generateViewSandbox", + platform = platform, + target = SandboxTarget.XML, + output = output, + multiplatform = false, + ) + attachToPreBuild(task, sandbox.autoGenerate.get()) + } + } +} + +private fun Project.sandboxOutputDirectory( + outputLocation: OutputLocation, + multiplatform: Boolean, +): Provider = when (outputLocation) { + OutputLocation.BUILD -> layout.buildDirectory.dir("generated/sdds/sandbox") + OutputLocation.SRC -> layout.dir( + providers.provider { + layout.projectDirectory.dir( + if (multiplatform) "src/commonMain/kotlin" else "src/main/kotlin", + ).asFile + }, + ) +} + +private fun Project.configureSandboxConventions( + extension: DsBuilderExtension, + platform: SandboxPlatform, +) { + platform.generatedPackageName.convention( + providers.provider { + extensions.findByType()?.namespace?.takeIf(String::isNotBlank) + ?: Gson().fromJson( + platform.componentsInfoFile.get().asFile.readText(), + Config::class.java, + ).packageName + ".sandbox" + }, + ) + platform.themeAlias.convention( + providers.provider { + val sdds = extension.sddsDirectory.get().asFile + SddsThemeSourceReader(sdds.parentFile, sdds).read().baseAlias + }, + ) +} + +private fun Project.registerSandboxTask( + name: String, + platform: SandboxPlatform, + target: SandboxTarget, + output: org.gradle.api.provider.Provider, + multiplatform: Boolean, +) = tasks.register(name) { + configInputFile.set(platform.componentsInfoFile) + packageName.set(platform.generatedPackageName) + themeAlias.set(platform.themeAlias) + scheme.set(platform.scheme) + this.target.set(target) + this.multiplatform.set(multiplatform) + outputDirectory.set(output) +}.also { task -> + if (!multiplatform) { + extensions.findByType() + ?.sourceSets + ?.maybeCreate("main") + ?.java + ?.srcDir(task.flatMap { it.outputDirectory }) + } +} + +private fun Project.configureDocumentation(extension: DsBuilderExtension) { + val coreSnippets = configurations.create("sddsCoreDocumentation") { + isCanBeConsumed = false + isCanBeResolved = true + description = "Versioned Core documentation artifacts used by DS Builder" + attributes { + attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage::class.java, Usage.JAVA_RUNTIME)) + attribute( + Category.CATEGORY_ATTRIBUTE, + objects.named(Category::class.java, Category.DOCUMENTATION), + ) + attribute(Attribute.of("com.sdds.docs.variant", String::class.java), "templates") + } + } + val compiler = configurations.create("sddsDocumentationKotlinCompiler") { + isCanBeConsumed = false + isCanBeResolved = true + description = "Isolated Kotlin compiler used to extract documentation examples" + } + dependencies.add( + compiler.name, + dependencies.create("org.jetbrains.kotlin:kotlin-compiler-embeddable:2.1.10"), + ) + + afterEvaluate { + val documentation = extension.documentation.takeIf { it.enabled.get() } ?: return@afterEvaluate + val platform = documentation.compose ?: documentation.view ?: return@afterEvaluate + val workDirectory = layout.buildDirectory.dir("sdds/documentation") + val extract = tasks.register("documentationExtract") { + group = "documentation" + description = "Extracts Kotlin and XML documentation examples" + kotlinCompiler.from(compiler) + xmlNamespace.set(extensions.findByType()?.namespace.orEmpty()) + outputKotlinDir.set(workDirectory.map { it.dir("kotlin") }) + outputXmlDir.set(workDirectory.map { it.dir("xml") }) + outputMeta.set(workDirectory.map { it.file("samples.json") }) + } + val aggregate = tasks.register("documentationAggregate") { + group = "documentation" + description = "Creates ADR-0003 Android documentation enrichment" + coreArtifacts.from(coreSnippets) + kotlinSnippets.set(extract.flatMap { it.outputKotlinDir }) + xmlSnippets.set(extract.flatMap { it.outputXmlDir }) + samplesMetadata.set(extract.flatMap { it.outputMeta }) + componentsInfoFile.set(platform.componentsInfoFile) + themeInfoFile.set(platform.themeInfoFile) + screenshotsDirectory.set( + layout.projectDirectory.dir("override-docs/static/screenshots-docusaurus"), + ) + userDocumentationRoot.set(documentation.userDocumentationRoot) + outputDirectory.set(documentation.outputDirectory) + dependsOn(extract) + } + attachToPreBuild(aggregate, documentation.autoGenerate.get()) + } +} + +private fun ThemeCapability.toLegacyExtension(root: DsBuilderExtension): ThemeBuilderExtension = + ThemeBuilderExtension().also { legacy -> + copyGenerationOptionsTo(legacy) + val resolvedSources = sources.orNull + ?: root.sddsDirectory.get().asFile.let { sddsDirectory -> + SddsThemeSourceReader(sddsDirectory.parentFile, sddsDirectory).read() + } + legacy.setThemeSources(resolvedSources) + legacy.paletteUrl = paletteUrl.get() + legacy.mode = mode.get() + legacy.autoGenerate = autoGenerate.get() + legacy.defaultThemeTypography = defaultTypography.get() + legacy.ignoreDisabledTokens = ignoreDisabledTokens.get() + legacy.useDefaultFonts = useDefaultFonts.get() + } + +private fun ComponentsCapability.toLegacyExtension(): ThemeBuilderExtension = + ThemeBuilderExtension().also { legacy -> + copyGenerationOptionsTo(legacy) + legacy.componentSource = source.orNull + legacy.componentsMetaStyleClass = componentsMetaStyleClass.get() + legacy.autoGenerate = autoGenerate.get() + } + +private fun GenerationCapability.copyGenerationOptionsTo(legacy: ThemeBuilderExtension) { + legacy.target = target() + legacy.ktPackage = packageName.orNull + legacy.resourcesPrefix = resourcesPrefix.orNull + legacy.viewThemeParents = viewThemeParents.get() + legacy.viewShapeAppearanceConfig = viewShapeAppearance.get() + legacy.outputLocation = outputLocation.get() + legacy.dimensionsConfig = dimensions.get() + legacy.multiplatform = multiplatform.get() +} + +private fun Project.attachToPreBuild(task: TaskProvider<*>, enabled: Boolean) { + if (!enabled) return + tasks.matching { it.name == "preBuild" }.configureEach { + dependsOn(task) + } +} diff --git a/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/GenerateComponentsTask.kt b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/GenerateComponentsTask.kt index 2a85984512..2d2d31f34a 100644 --- a/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/GenerateComponentsTask.kt +++ b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/GenerateComponentsTask.kt @@ -36,6 +36,7 @@ import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputDirectory import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Internal import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction @@ -77,7 +78,7 @@ internal abstract class GenerateComponentsTask : DefaultTask() { /** * Директория проекта */ - @get:OutputDirectory + @get:Internal abstract val projectDir: DirectoryProperty /** diff --git a/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/GenerateThemeTask.kt b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/GenerateThemeTask.kt index 5660aecbee..df639c1007 100644 --- a/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/GenerateThemeTask.kt +++ b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/GenerateThemeTask.kt @@ -50,6 +50,7 @@ import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFile import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Internal import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction import org.gradle.kotlin.dsl.provideDelegate @@ -172,7 +173,7 @@ abstract class GenerateThemeTask : DefaultTask() { /** * Директория проекта */ - @get:OutputDirectory + @get:Internal abstract val projectDir: DirectoryProperty /** diff --git a/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/SddsDirectoryResolver.kt b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/SddsDirectoryResolver.kt new file mode 100644 index 0000000000..4bd6f91f79 --- /dev/null +++ b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/SddsDirectoryResolver.kt @@ -0,0 +1,72 @@ +package com.sdds.plugin.themebuilder + +import org.gradle.api.GradleException +import java.io.File + +/** Android UI platform used to resolve standard DS Builder info artifacts. */ +enum class DsBuilderPlatform( + internal val componentsInfoName: String, + internal val themeInfoName: String, +) { + /** Jetpack Compose. */ + COMPOSE("config-info-compose.json", "theme-info-compose.json"), + + /** Android View system. */ + VIEW("config-info-view-system.json", "theme-info-view-system.json"), +} + +/** + * Resolves `.sdds` from the current project and its immediate Gradle parent. + * + * The parent directory is supplied explicitly because a Gradle parent project + * does not necessarily coincide with the filesystem parent. + */ +class SddsDirectoryResolver( + private val projectDirectory: File, + private val parentProjectDirectory: File? = null, +) { + private val candidates: List + get() = listOfNotNull( + projectDirectory.resolve(SDDS_DIRECTORY_NAME), + parentProjectDirectory?.resolve(SDDS_DIRECTORY_NAME), + ).distinct() + + /** Returns the first existing standard `.sdds` directory, if any. */ + fun resolveOrNull(): File? = candidates.firstOrNull(File::isDirectory) + + /** + * Returns the standard `.sdds` directory or fails with every checked path. + */ + fun resolve(): File = resolveOrNull() + ?: throw GradleException( + buildString { + append("Cannot resolve .sdds directory. Checked: ") + append(candidates.joinToString { it.absolutePath }) + append(". Set dsBuilder.sddsDirectory explicitly or create one of these directories.") + }, + ) + + /** Resolves `.sdds/config.json` and optionally verifies it exists. */ + fun configFile(required: Boolean = true): File = + resolve().resolve(CONFIG_FILE_NAME).requireFile("DS Builder config", required) + + /** Resolves the platform-specific components info file. */ + fun componentsInfoFile(platform: DsBuilderPlatform, required: Boolean = true): File = + resolve().resolve(platform.componentsInfoName).requireFile("Components info", required) + + /** Resolves the platform-specific theme info file. */ + fun themeInfoFile(platform: DsBuilderPlatform, required: Boolean = true): File = + resolve().resolve(platform.themeInfoName).requireFile("Theme info", required) + + private fun File.requireFile(description: String, required: Boolean): File { + if (required && !isFile) { + throw GradleException("$description file does not exist: $absolutePath") + } + return this + } + + private companion object { + const val SDDS_DIRECTORY_NAME = ".sdds" + const val CONFIG_FILE_NAME = "config.json" + } +} diff --git a/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/SddsThemeSourceReader.kt b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/SddsThemeSourceReader.kt index 8ca7772a90..760285b1e9 100644 --- a/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/SddsThemeSourceReader.kt +++ b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/SddsThemeSourceReader.kt @@ -10,17 +10,18 @@ import java.io.File */ internal class SddsThemeSourceReader( private val projectDir: File, + private val sddsDirectory: File = projectDir.resolve(SDDS_DIR), ) { fun read(): ThemeBuilderSources { val configFile = getConfigFile() val config = json.decodeFromString(SddsConfig.serializer(), configFile.readText()) validateTenants(config) - val paletteFile = config.paletteFile(projectDir) + val paletteFile = config.paletteFile(projectDir, sddsDirectory) val sources = config.tenants.mapIndexed { index, tenant -> val tenantName = tenant.publicName - val tenantDirectory = tenant.directory(projectDir) + val tenantDirectory = tenant.directory(projectDir, sddsDirectory) ThemeBuilderSource.withLocalDirectory( directory = tenantDirectory, name = tenantName, @@ -36,7 +37,7 @@ internal class SddsThemeSourceReader( } private fun getConfigFile(): File { - val configFile = projectDir.resolve(CONFIG_PATH) + val configFile = sddsDirectory.resolve(CONFIG_FILE_NAME) if (!configFile.exists()) { throw ThemeBuilderException( "themeSource(s) or $CONFIG_PATH must be provided. Missing file: ${configFile.path}", @@ -56,9 +57,9 @@ internal class SddsThemeSourceReader( val tenants: List = emptyList(), val palettePath: String? = null, ) { - fun paletteFile(projectDir: File): File { - val path = palettePath ?: DEFAULT_PALETTE_PATH - return projectDir.resolve(path) + fun paletteFile(projectDir: File, sddsDirectory: File): File { + return palettePath?.let(projectDir::resolve) + ?: sddsDirectory.resolve(DEFAULT_PALETTE_RELATIVE_PATH) } } @@ -71,16 +72,17 @@ internal class SddsThemeSourceReader( val publicName: String get() = alias ?: name - fun directory(projectDir: File): File { - val path = directoryPath ?: "$SDDS_DIR/$name" - return projectDir.resolve(path) + fun directory(projectDir: File, sddsDirectory: File): File { + return directoryPath?.let(projectDir::resolve) + ?: sddsDirectory.resolve(name) } } private companion object { const val SDDS_DIR = ".sdds" + const val CONFIG_FILE_NAME = "config.json" const val CONFIG_PATH = "$SDDS_DIR/config.json" - const val DEFAULT_PALETTE_PATH = "$SDDS_DIR/tenants/palette.json" + const val DEFAULT_PALETTE_RELATIVE_PATH = "tenants/palette.json" val json = Json { ignoreUnknownKeys = true diff --git a/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/ThemeBuilderExtension.kt b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/ThemeBuilderExtension.kt index e357f3352c..41ad540f01 100644 --- a/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/ThemeBuilderExtension.kt +++ b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/ThemeBuilderExtension.kt @@ -3,7 +3,6 @@ package com.sdds.plugin.themebuilder import com.sdds.plugin.themebuilder.ShapeAppearanceConfig.Companion.materialShape import com.sdds.plugin.themebuilder.internal.ThemeBuilderTarget import com.sdds.plugin.themebuilder.internal.exceptions.ThemeBuilderException -import org.gradle.api.Project import java.io.Serializable /** @@ -134,6 +133,11 @@ open class ThemeBuilderExtension { } } + internal fun setThemeSources(sources: ThemeBuilderSources) { + themeSource = null + themeSources = sources + } + /** * Устанавливает View фреймворк для генерации темы и токенов * @@ -242,16 +246,9 @@ open class ThemeBuilderExtension { } } - companion object { + private companion object { private const val DEFAULT_PALETTE_URL = "https://raw.githubusercontent.com/salute-developers/plasma/dev/packages/plasma-colors/palette/general.json" - - /** - * Создает extension [ThemeBuilderExtension] - */ - fun Project.themeBuilderExt(): ThemeBuilderExtension { - return extensions.create("themeBuilder", ThemeBuilderExtension::class.java) - } } } diff --git a/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/ThemeBuilderPlugin.kt b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/ThemeBuilderPlugin.kt index c156a83e65..18f5d629ab 100644 --- a/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/ThemeBuilderPlugin.kt +++ b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/ThemeBuilderPlugin.kt @@ -6,9 +6,7 @@ import com.android.build.gradle.BaseExtension import com.android.build.gradle.LibraryExtension import com.android.build.gradle.LibraryPlugin import com.android.build.gradle.internal.tasks.factory.dependsOn -import com.sdds.plugin.themebuilder.ThemeBuilderExtension.Companion.themeBuilderExt import org.gradle.api.GradleException -import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.artifacts.Configuration import org.gradle.api.attributes.Attribute @@ -26,66 +24,87 @@ import java.io.File * Плагин для генерации тем и токенов ДС * @author Малышев Александр on 09.02.2024 */ -class ThemeBuilderPlugin : Plugin { +internal class ThemeBuilderPlugin { - override fun apply(project: Project) { + internal fun configure( + project: Project, + themeExtension: () -> ThemeBuilderExtension?, + componentsExtension: () -> ThemeBuilderExtension?, + ) { val componentsZip = project.layout.buildDirectory.file("$COMPONENTS_PATH/components.zip") val paletteJson = project.layout.buildDirectory.file("$THEME_PATH/$PALETTE_JSON_NAME") - val extension = project.themeBuilderExt() project.configureSourceSets() val readUikitApiMetaTask = project.registerApiMetaTask() project.afterEvaluate { - val compileClasspath = listOf( - "debugCompileClasspath", - "releaseCompileClasspath", - "compileClasspath", - ).firstNotNullOfOrNull { project.configurations.findByName(it) } ?: return@afterEvaluate - readUikitApiMetaTask.configureUikitApiMetaTask(compileClasspath) - - project.registerClean(extension) - val themeSources = ThemeSourceResolver(project.projectDir) - .resolve(extension.getThemeSourcesOrNull()) - val paletteFile = themeSources.paletteFile ?: paletteJson.get().asFile - val fetchPaletteTask = if (themeSources.paletteFile == null) { - registerPaletteFetcher( - taskName = "fetchPalette", - paletteUrl = extension.paletteUrl, - paletteOutput = paletteJson, - ) - } else { - null + val theme = themeExtension() + val components = componentsExtension() + val cleanExtension = theme ?: components + if (cleanExtension != null) { + project.registerClean(cleanExtension) } - val unzipTasks = mutableListOf>() - themeSources.sources.forEach { source -> - if (source is ThemeBuilderSource.LocalDirectory) { - return@forEach + if (theme != null) { + val themeSources = ThemeSourceResolver(project.projectDir) + .resolve(theme.getThemeSourcesOrNull()) + val paletteFile = themeSources.paletteFile ?: paletteJson.get().asFile + val fetchPaletteTask = if (themeSources.paletteFile == null) { + registerPaletteFetcher( + taskName = "fetchPalette", + paletteUrl = theme.paletteUrl, + paletteOutput = paletteJson, + ) + } else { + null + } + + val unzipTasks = mutableListOf>() + themeSources.sources.forEach { source -> + if (source is ThemeBuilderSource.LocalDirectory) { + return@forEach + } + val tenantId = source.tenant + val tenantIdForPath = if (tenantId.isEmpty()) "default" else tenantId.lowercase() + val themeZip = project.layout.buildDirectory.file("$THEME_PATH/$tenantIdForPath/theme.zip") + + val unzipThemeTask = registerFetchAndUnzipTheme( + source = source, + themeOutputZip = themeZip, + themeId = tenantId, + fetchPaletteTask = fetchPaletteTask, + ) + unzipTasks.add(unzipThemeTask) } - val tenantId = source.tenant - val tenantIdForPath = if (tenantId.isEmpty()) "default" else tenantId.lowercase() - val themeZip = project.layout.buildDirectory.file("$THEME_PATH/$tenantIdForPath/theme.zip") - - val unzipThemeTask = registerFetchAndUnzipTheme( - source = source, - themeOutputZip = themeZip, - themeId = tenantId, - fetchPaletteTask = fetchPaletteTask, + + registerThemeBuilder( + extension = theme, + unzipThemeTasks = unzipTasks, + dependOnPreBuild = theme.autoGenerate, + themeSources = themeSources, + paletteFile = paletteFile, ) - unzipTasks.add(unzipThemeTask) } - registerThemeBuilder( - extension = extension, - unzipThemeTasks = unzipTasks, - dependOnPreBuild = extension.autoGenerate, - themeSources = themeSources, - paletteFile = paletteFile, - ) - - val fetchComponentsTask = registerFetchAndUnzipComponents(extension, componentsZip) - fetchComponentsTask?.let { registerGenerateComponentsTask(extension, it, readUikitApiMetaTask) } + if (components != null) { + val compileClasspath = listOf( + "debugCompileClasspath", + "releaseCompileClasspath", + "compileClasspath", + ).firstNotNullOfOrNull { project.configurations.findByName(it) } + if (compileClasspath != null) { + readUikitApiMetaTask.configureUikitApiMetaTask(compileClasspath) + } + val fetchComponentsTask = registerFetchAndUnzipComponents(components, componentsZip) + fetchComponentsTask?.let { + val generateComponents = registerGenerateComponentsTask(components, it, readUikitApiMetaTask) + if (components.autoGenerate) { + tasks.matching { task -> task.name == "preBuild" }.configureEach { + dependsOn(generateComponents) + } + } + } + } } } @@ -175,7 +194,7 @@ class ThemeBuilderPlugin : Plugin { extension: ThemeBuilderExtension, fetchComponentsTask: TaskProvider, readUikitApiMetaTask: TaskProvider, - ) { + ): TaskProvider { val task = project.tasks.register("generateComponents") { group = TASK_GROUP componentsDir.set(getComponentsDir()) @@ -195,6 +214,7 @@ class ThemeBuilderPlugin : Plugin { uikitApiMetaFile.set(readUikitApiMetaTask.flatMap { it.outputFile }) } task.dependsOn(fetchComponentsTask, readUikitApiMetaTask) + return task } private fun Project.registerThemeBuilder( diff --git a/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/documentation/ActionUsingKotlinCompiler.kt b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/documentation/ActionUsingKotlinCompiler.kt new file mode 100644 index 0000000000..761776302a --- /dev/null +++ b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/documentation/ActionUsingKotlinCompiler.kt @@ -0,0 +1,94 @@ +@file:Suppress( + "UndocumentedPublicClass", + "UndocumentedPublicProperty", +) + +package com.sdds.plugin.themebuilder.documentation + +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.workers.WorkAction +import org.gradle.workers.WorkParameters +import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import org.jetbrains.kotlin.com.intellij.openapi.util.Disposer +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.config.KotlinCompilerVersion +import org.jetbrains.kotlin.psi.KtPsiFactory +import org.slf4j.LoggerFactory +import java.io.File + +abstract class ActionUsingKotlinCompiler : WorkAction { + + interface Params : WorkParameters { + val kotlinSources: ConfigurableFileCollection + val xmlSources: ConfigurableFileCollection + val outputKotlinDir: DirectoryProperty + val outputXmlDir: DirectoryProperty + val outputMeta: RegularFileProperty + val namespace: Property + val projectDir: DirectoryProperty + } + + private val logger = LoggerFactory.getLogger(ActionUsingKotlinCompiler::class.java) + + override fun execute() { + logger.info("Kotlin compiler version: ${KotlinCompilerVersion.getVersion()}") + + val outKotlinDir = parameters.outputKotlinDir.get().asFile + outKotlinDir.mkdirs() + + val outXmlDir = parameters.outputXmlDir.get().asFile + outXmlDir.mkdirs() + + val kotlinFiles = parameters.kotlinSources.files.toList() + val xmlFiles = parameters.xmlSources.files.toList() + val meta = mutableListOf() + + val disposable = Disposer.newDisposable("sdds-docs-psi") + try { + val env = KotlinCoreEnvironment.createForProduction( + disposable, + CompilerConfiguration(), + EnvironmentConfigFiles.JVM_CONFIG_FILES, + ) + val projectDir = parameters.projectDir.get().asFile + + val psiFactory = KtPsiFactory(env.project, markGenerated = false) + + val kotlinDelegate = KotlinSnippetExtractorDelegate( + psiFactory = psiFactory, + snippetsDir = outKotlinDir, + projectDir = projectDir, + ) + + kotlinFiles.forEach { file -> + meta += kotlinDelegate.extractFromFile(file) + } + + val xmlDelegate = XmlSnippetExtractorDelegate( + snippetsDir = outXmlDir, + projectDir = projectDir, + namespace = parameters.namespace.get(), + ) + + xmlFiles.forEach { file -> + meta += xmlDelegate.extractFromFile(file) + } + + val metaFile = parameters.outputMeta.asFile.get() + metaFile.writeText(Json { prettyPrint = true }.encodeToString(meta)) + logger.info("Written docs meta: ${relativePath(projectDir, metaFile)} (samples=${meta.size})") + } finally { + Disposer.dispose(disposable) + } + } +} + +internal fun relativePath(dir: File, target: File): String { + return dir.toURI().relativize(target.toURI()).path +} diff --git a/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/documentation/DocumentationAggregateTask.kt b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/documentation/DocumentationAggregateTask.kt new file mode 100644 index 0000000000..6f915d0836 --- /dev/null +++ b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/documentation/DocumentationAggregateTask.kt @@ -0,0 +1,634 @@ +package com.sdds.plugin.themebuilder.documentation + +import com.google.gson.GsonBuilder +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import java.io.File +import java.util.zip.ZipEntry +import java.util.zip.ZipFile + +/** Assembles Android documentation enrichment using the ADR-0003 directory contract. */ +abstract class DocumentationAggregateTask : DefaultTask() { + + /** Versioned Core documentation template JARs. */ + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val coreArtifacts: ConfigurableFileCollection + + /** Locally extracted Kotlin examples. */ + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val kotlinSnippets: DirectoryProperty + + /** Locally extracted XML examples. */ + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val xmlSnippets: DirectoryProperty + + /** Metadata describing local examples. */ + @get:InputFile + @get:PathSensitive(PathSensitivity.NONE) + abstract val samplesMetadata: RegularFileProperty + + /** Platform components info JSON. */ + @get:InputFile + @get:PathSensitive(PathSensitivity.NONE) + abstract val componentsInfoFile: RegularFileProperty + + /** Platform theme info JSON. */ + @get:InputFile + @get:PathSensitive(PathSensitivity.NONE) + abstract val themeInfoFile: RegularFileProperty + + /** Locally generated screenshots keyed by screenshot directives. */ + @get:InputDirectory + @get:Optional + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val screenshotsDirectory: DirectoryProperty + + /** Optional user documentation root containing `structure.json` and `docs/**/*.md`. */ + @get:InputDirectory + @get:Optional + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val userDocumentationRoot: DirectoryProperty + + /** `.sdds/temp/docs` directory consumed by DS Builder CLI. */ + @get:OutputDirectory + abstract val outputDirectory: DirectoryProperty + + /** + * Builds a deterministic ADR-0003 enrichment tree. + * + * Local examples and metadata override their Core counterparts. + */ + @TaskAction + fun aggregate() { + val output = outputDirectory.get().asFile + output.deleteRecursively() + output.mkdirs() + + val core = unpackCoreArtifacts(output) + copyTree(kotlinSnippets.get().asFile, output.resolve("assets/examples/kotlin")) + copyTree(xmlSnippets.get().asFile, output.resolve("assets/examples/xml")) + screenshotsDirectory.orNull?.asFile + ?.takeIf(File::isDirectory) + ?.let { copyTree(it, output.resolve("assets/screenshots")) } + mergeSamples( + coreSamples = core.samples, + localMetadata = samplesMetadata.get().asFile, + target = output.resolve("meta/samples.json"), + ) + copyRequired(componentsInfoFile.get().asFile, output.resolve("meta/components-info.json")) + copyRequired(themeInfoFile.get().asFile, output.resolve("meta/theme-info.json")) + enrichDocumentation(core, output) + } + + private fun unpackCoreArtifacts(output: File): CoreDocumentation { + val core = CoreDocumentation() + coreArtifacts.files.sortedBy(File::getAbsolutePath).forEach { artifact -> + ZipFile(artifact).use { zip -> + zip.entries().asSequence() + .filterNot { it.isDirectory } + .filter { it.name.startsWith(CORE_DOCS_PREFIX) } + .sortedBy { it.name } + .forEach { entry -> processCoreEntry(zip, entry, output, core) } + } + } + return core + } + + @Suppress("CyclomaticComplexMethod", "ThrowsCount") + private fun processCoreEntry( + zip: ZipFile, + entry: ZipEntry, + output: File, + core: CoreDocumentation, + ) { + val relativePath = normalizeRelativePath( + entry.name.removePrefix(CORE_DOCS_PREFIX), + "Core documentation entry ${entry.name}", + ) + when { + relativePath == LEGACY_META_PATH || relativePath == SAMPLES_META_PATH -> { + zip.getInputStream(entry).reader().use { reader -> + core.samples += parseSamples(JsonParser.parseReader(reader)) + } + } + relativePath == STRUCTURE_PATH -> { + if (core.structure != null) { + throw GradleException("Conflicting Core documentation structure: ${entry.name}") + } + val structure = zip.getInputStream(entry).reader().use(JsonParser::parseReader) + if (!structure.isJsonObject) { + throw GradleException("Core documentation structure must be a JSON object: ${entry.name}") + } + core.structure = structure.asJsonObject + } + relativePath.startsWith(TEMPLATES_PREFIX) && relativePath.endsWith(".md") -> { + if (core.templates.containsKey(relativePath)) { + throw GradleException("Conflicting Core documentation template: $relativePath") + } + core.templates[relativePath] = zip.getInputStream(entry).reader().use { it.readText() } + } + relativePath.startsWith(EXAMPLES_PREFIX) -> { + copyZipEntryChecked(zip, entry, checkedTarget(output, relativePath, entry.name)) + } + relativePath.startsWith("meta/") || relativePath.startsWith("assets/") -> Unit + else -> { + copyZipEntry( + zip, + entry, + checkedTarget(output, "assets/examples/kotlin/$relativePath", entry.name), + ) + } + } + } + + private fun copyZipEntry(zip: ZipFile, entry: ZipEntry, target: File) { + target.parentFile.mkdirs() + zip.getInputStream(entry).use { input -> + target.outputStream().use(input::copyTo) + } + } + + private fun copyZipEntryChecked(zip: ZipFile, entry: ZipEntry, target: File) { + val bytes = zip.getInputStream(entry).use { it.readBytes() } + if (target.isFile) { + if (!target.readBytes().contentEquals(bytes)) { + throw GradleException("Conflicting documentation asset: ${entry.name}") + } + return + } + target.parentFile.mkdirs() + target.writeBytes(bytes) + } + + private fun checkedTarget(output: File, relativePath: String, source: String): File { + val target = output.resolve(relativePath).canonicalFile + if (!target.toPath().startsWith(output.canonicalFile.toPath())) { + throw GradleException("Invalid Core documentation entry: $source") + } + return target + } + + private fun copyTree(source: File, target: File) { + target.mkdirs() + if (!source.exists()) return + source.walkTopDown().filter(File::isFile).sortedBy { it.relativeTo(source).path }.forEach { file -> + val destination = target.resolve(file.relativeTo(source).path) + destination.parentFile.mkdirs() + file.copyTo(destination, overwrite = true) + } + } + + private fun copyRequired(source: File, target: File) { + if (!source.isFile) { + throw GradleException("Required documentation input does not exist: ${source.absolutePath}") + } + target.parentFile.mkdirs() + source.copyTo(target, overwrite = true) + } + + private fun mergeSamples( + coreSamples: List, + localMetadata: File, + target: File, + ) { + if (!localMetadata.isFile) { + throw GradleException("Required documentation input does not exist: ${localMetadata.absolutePath}") + } + val localSamples = localMetadata.reader().use { reader -> + parseSamples(JsonParser.parseReader(reader)) + } + val merged = linkedMapOf() + (coreSamples + localSamples).map(::normalizeSamplePath).forEachIndexed { index, sample -> + val key = sample.get("id")?.asString ?: "__anonymous_$index" + merged[key] = sample + } + target.parentFile.mkdirs() + target.writeText(GSON.toJson(merged.values)) + } + + @Suppress("ThrowsCount") + private fun enrichDocumentation(core: CoreDocumentation, output: File) { + val structure = core.structure + if (structure == null) { + if (core.templates.isNotEmpty()) { + throw GradleException("Core documentation templates require structure.json") + } + enrichUserDocumentation(emptySet(), output) + return + } + val styleApis = resolveStyleApis(componentsInfoFile.get().asFile) + val publicPaths = collectPublicPages(structure, "Core").mapTo(linkedSetOf(), Page::path) + publicPaths.forEach { pagePath -> + val templatePath = "$TEMPLATES_PREFIX$pagePath" + val template = core.templates[templatePath] + ?: throw GradleException("Core documentation template does not exist: $pagePath") + val target = checkedTarget(output, "content/core/$pagePath", templatePath) + target.parentFile.mkdirs() + target.writeText(enrichTemplate(template, "core", pagePath, output, styleApis)) + } + val structureTarget = checkedTarget(output, "structure-core.json", STRUCTURE_PATH) + structureTarget.parentFile.mkdirs() + structureTarget.writeText(GSON.toJson(structure)) + enrichUserDocumentation(publicPaths, output) + } + + @Suppress("CyclomaticComplexMethod", "ThrowsCount") + private fun enrichUserDocumentation(corePaths: Set, output: File) { + val root = userDocumentationRoot.orNull?.asFile ?: return + val structureFile = root.resolve(STRUCTURE_PATH) + if (!structureFile.isFile) return + val structureElement = runCatching { JsonParser.parseString(structureFile.readText()) } + .getOrElse { throw GradleException("Invalid user documentation structure: ${structureFile.path}", it) } + if (!structureElement.isJsonObject) { + throw GradleException("User documentation structure must be a JSON object: ${structureFile.path}") + } + val structure = structureElement.asJsonObject + val pages = collectPublicPages(structure, "User") + val styleApis = resolveStyleApis(componentsInfoFile.get().asFile) + pages.forEach { page -> + val collidesWithCore = page.path in corePaths + val merge = page.merge + when { + merge == "prepend" -> throw GradleException( + "User documentation page '${page.path}' uses unsupported merge mode: prepend", + ) + merge != null && merge !in SUPPORTED_MERGES -> throw GradleException( + "User documentation page '${page.path}' uses unsupported merge mode: $merge", + ) + collidesWithCore && merge == null -> throw GradleException( + "User documentation page '${page.path}' collides with Core and requires explicit append or replace", + ) + !collidesWithCore && merge != null -> throw GradleException( + "Standalone user documentation page '${page.path}' must not declare merge: $merge", + ) + } + val sourcePath = when (merge) { + "append" -> page.path.withPlusFileName() + else -> page.path + } + val source = checkedSource(root.resolve(TEMPLATES_PREFIX), sourcePath, page.path) + if (!source.isFile) { + val alternatePath = if (merge == "append") page.path else page.path.withPlusFileName() + val alternate = checkedSource(root.resolve(TEMPLATES_PREFIX), alternatePath, page.path) + val message = when { + merge == "append" && alternate.isFile -> + "Append user documentation page '${page.path}' must use source '$sourcePath'" + !collidesWithCore && alternate.isFile -> + "Standalone user documentation page '${page.path}' " + + "must not use plus-prefixed source '$alternatePath'" + else -> "User documentation source does not exist for '${page.path}': ${source.path}" + } + throw GradleException(message) + } + val target = checkedTarget(output, "content/user/${page.path}", source.path) + target.parentFile.mkdirs() + target.writeText(enrichTemplate(source.readText(), "user", page.path, output, styleApis)) + } + checkedTarget(output, "structure-user.json", structureFile.path) + .writeText(GSON.toJson(structure)) + } + + private fun checkedSource(root: File, relativePath: String, logicalPath: String): File { + val source = root.resolve(relativePath).canonicalFile + if (!source.toPath().startsWith(root.canonicalFile.toPath())) { + throw GradleException("User documentation page '$logicalPath' has an unsafe source path") + } + return source + } + + private fun enrichTemplate( + template: String, + layer: String, + logicalPath: String, + output: File, + styleApis: StyleApis, + ): String { + return enrichSamples(template, layer, logicalPath, output) + .replace(STYLE_API_REGEX) { + renderStyleApi(logicalPath, styleApis) + } + } + + private fun collectPublicPages(structure: JsonObject, layer: String): List { + val navigation = structure.get("navigation") + if (navigation == null || !navigation.isJsonArray) { + throw GradleException("$layer documentation structure must contain a navigation array") + } + val result = mutableListOf() + navigation.asJsonArray.forEach { collectPages(it, layer, result) } + return result + } + + @Suppress("ThrowsCount") + private fun collectPages(element: JsonElement, layer: String, result: MutableList) { + if (!element.isJsonObject) { + throw GradleException("$layer documentation navigation entries must be JSON objects") + } + val node = element.asJsonObject + node.get("path")?.let { pathElement -> + if (!pathElement.isJsonPrimitive || !pathElement.asJsonPrimitive.isString) { + throw GradleException("$layer documentation page path must be a string") + } + val path = normalizeRelativePath(pathElement.asString, "$layer documentation page path") + if (!path.endsWith(".md")) { + throw GradleException("$layer documentation page must reference markdown: $path") + } + if (File(path).name.startsWith("+")) { + throw GradleException("$layer documentation logical path must not use plus prefix: $path") + } + val merge = node.get("merge")?.let { + if (!it.isJsonPrimitive || !it.asJsonPrimitive.isString) { + throw GradleException("$layer documentation merge must be a string: $path") + } + it.asString + } + result += Page(path, merge) + } + node.get("items")?.let { items -> + if (!items.isJsonArray) { + throw GradleException("$layer documentation navigation items must be an array") + } + items.asJsonArray.forEach { collectPages(it, layer, result) } + } + } + + private fun enrichSamples(template: String, layer: String, logicalPath: String, output: File): String { + val withKotlin = template.replace(KOTLIN_SAMPLE_REGEX) { match -> + resolveSample(match.groupValues[1].trim(), "kotlin", layer, logicalPath, output) + } + return withKotlin.replace(XML_SAMPLE_REGEX) { match -> + resolveSample(match.groupValues[1].trim(), "xml", layer, logicalPath, output) + } + } + + private fun resolveSample( + reference: String, + language: String, + layer: String, + logicalPath: String, + output: File, + ): String { + val normalized = normalizeRelativePath(reference, "Sample reference in $layer page $logicalPath") + val examples = output.resolve("assets/examples") + val candidates = if (normalized.startsWith("assets/")) { + listOf(output.resolve(normalized)) + } else { + listOf(examples.resolve(normalized), examples.resolve(language).resolve(normalized)) + } + val sample = candidates.firstOrNull(File::isFile) + ?: throw GradleException( + "Documentation sample '$reference' referenced from $layer page '$logicalPath' does not exist", + ) + return sample.readText().trim() + } + + private fun normalizeRelativePath(path: String, description: String): String { + val normalized = path.replace('\\', '/') + val parts = normalized.split('/').filter(String::isNotEmpty) + val hasInvalidSegment = parts.any { it == "." || it == ".." } + if (normalized.startsWith("/") || parts.isEmpty() || hasInvalidSegment) { + throw GradleException("$description has an invalid relative path: $path") + } + return parts.joinToString("/") + } + + private fun normalizeSamplePath(sample: JsonObject): JsonObject { + val normalized = sample.deepCopy() + val snippetPath = normalized.get("snippetPath")?.asString + ?.replace('\\', '/') + ?.takeIf(String::isNotBlank) + ?: return normalized + if (snippetPath.startsWith("assets/")) return normalized + val language = if (normalized.get("kind")?.asString == "xml") "xml" else "kotlin" + normalized.addProperty("snippetPath", "assets/examples/$language/$snippetPath") + return normalized + } + + private fun resolveStyleApis(componentsInfo: File): StyleApis { + val info = JsonParser.parseString(componentsInfo.readText()) + if (!info.isJsonObject) return StyleApis() + val components = info.asJsonObject.getAsJsonArrayOrEmpty("components") + val componentNames = components.mapNotNull { element -> + element.takeIf(JsonElement::isJsonObject)?.asJsonObject?.get("coreName")?.asString + }.toSet() + val docs = components.mapNotNull { element -> + val component = element.asJsonObject + val styleApi = component.getAsJsonObjectOrNull("styleApi") ?: return@mapNotNull null + StyleApiDoc( + coreName = component.get("coreName").asString, + styleName = component.get("styleName").asString, + receiverClassName = styleApi.get("receiverClassName").asString, + functionName = styleApi.get("functionName")?.asString ?: "style", + params = styleApi.getAsJsonArrayOrEmpty("params").map { paramElement -> + val param = paramElement.asJsonObject + StyleApiParamDoc( + name = param.get("name").asString, + typeName = param.get("typeName").asString, + defaultValue = param.getAsJsonObjectOrNull("defaultValue")?.toStyleApiValueDoc(), + values = param.getAsJsonArrayOrEmpty("values").map { + it.asJsonObject.toStyleApiValueDoc() + }, + ) + }, + variations = component.getAsJsonArrayOrEmpty("variations").mapNotNull variation@{ variationElement -> + val variation = variationElement.asJsonObject + val reference = variation.get("composeReference") + ?.takeUnless(JsonElement::isJsonNull) + ?.asString + ?: return@variation null + StyleVariationDoc( + composeReference = reference, + props = variation.getAsJsonArrayOrEmpty("props").associate { + val property = it.asJsonObject + property.get("name").asString to property.get("value").asString + }, + ) + }, + ) + }.groupBy(StyleApiDoc::coreName) + return StyleApis(componentNames, docs) + } + + private fun renderStyleApi(templatePath: String, styleApis: StyleApis): String { + val componentName = File(templatePath).name.removeSuffix("Usage.md") + val hasStyles = styleApis.componentNames.any { it.equals(componentName, ignoreCase = true) } + val docs = styleApis.docs[componentName].orEmpty() + return when { + hasStyles && docs.isNotEmpty() -> docs.joinToString("\n\n", transform = StyleApiDoc::toMarkdown) + hasStyles -> "" + else -> """ + :::warning + У компонента нет готовых стилей. Если нужен стиль, обратитесь в поддержку. + ::: + """.trimIndent() + } + } + + private fun parseSamples(element: JsonElement): List { + if (!element.isJsonArray) { + throw GradleException("Snippet metadata must be a JSON array") + } + return element.asJsonArray.map { item -> + if (!item.isJsonObject) { + throw GradleException("Snippet metadata entries must be JSON objects") + } + item.asJsonObject + } + } + + private data class CoreDocumentation( + var structure: JsonObject? = null, + val templates: MutableMap = linkedMapOf(), + val samples: MutableList = mutableListOf(), + ) + + private data class Page(val path: String, val merge: String?) + + private data class StyleApis( + val componentNames: Set = emptySet(), + val docs: Map> = emptyMap(), + ) + + private data class StyleApiDoc( + val coreName: String, + val styleName: String, + val receiverClassName: String, + val functionName: String, + val params: List, + val variations: List, + ) { + fun toMarkdown(): String { + val header = if (styleName == coreName) { + "### Параметры стиля" + } else { + "### Параметры стиля `$styleName`" + } + return buildString { + if (params.isNotEmpty()) { + appendLine(header) + appendLine() + appendLine("| Параметр | Тип | Возможные значения |") + appendLine("| --- | --- | --- |") + params.forEach { param -> + appendLine("| `${param.name}` | `${param.typeName}` | ${param.valuesColumn} |") + } + appendLine() + } + appendLine("Пример выбора готового стиля:") + appendLine("```kotlin") + append(exampleCall()) + exampleDotNotationCall()?.let { dotExample -> + appendLine() + appendLine() + appendLine("// или через dot notation") + appendLine(dotExample) + } + appendLine("```") + } + } + + private fun exampleCall(): String { + val invocation = receiverClassName.removeSuffix(".Companion") + val args = params.joinToString(",\n") { param -> + " ${param.name} = ${param.exampleValueExpression()}" + } + return buildString { + append("val style = $invocation.$functionName(") + if (args.isNotBlank()) append("\n$args\n") + append(")") + } + } + + private fun exampleDotNotationCall(): String? { + val reference = variations.firstOrNull { variation -> + params.all { param -> + val actual = variation.props[param.name] ?: param.defaultValue?.value + actual == param.exampleRawValue() + } + }?.composeReference ?: variations.firstOrNull()?.composeReference + return reference?.let { "val style = $it.style()" } + } + } + + private data class StyleApiParamDoc( + val name: String, + val typeName: String, + val defaultValue: StyleApiValueDoc?, + val values: List, + ) { + val valuesColumn: String + get() = if (values.isEmpty()) "-" else values.joinToString(", ") { "`${it.codeName}`" } + + fun exampleValueExpression(): String { + val selected = defaultValue ?: values.firstOrNull() + ?: throw GradleException("No values available for style parameter '$name'") + return if (typeName == "Boolean") selected.codeName else "$typeName.${selected.codeName}" + } + + fun exampleRawValue(): String { + return (defaultValue ?: values.firstOrNull())?.value + ?: throw GradleException("No values available for style parameter '$name'") + } + } + + internal data class StyleApiValueDoc(val value: String, val codeName: String) + + private data class StyleVariationDoc( + val composeReference: String, + val props: Map, + ) + + private companion object { + const val CORE_DOCS_PREFIX = "META-INF/sdds-docs/" + const val STRUCTURE_PATH = "structure.json" + const val TEMPLATES_PREFIX = "docs/" + const val EXAMPLES_PREFIX = "assets/examples/" + const val LEGACY_META_PATH = "meta.json" + const val SAMPLES_META_PATH = "meta/samples.json" + val SUPPORTED_MERGES = setOf("append", "replace") + val KOTLIN_SAMPLE_REGEX = "//\\s*@sample:\\s*(.+)".toRegex() + val XML_SAMPLE_REGEX = "".toRegex() + val STYLE_API_REGEX = "".toRegex() + val GSON = GsonBuilder().setPrettyPrinting().create() + } +} + +private fun String.withPlusFileName(): String { + val file = File(this) + return file.parent?.let { "$it/+${file.name}" } ?: "+${file.name}" +} + +private fun JsonObject.getAsJsonObjectOrNull(name: String): JsonObject? { + val element = get(name) ?: return null + return if (element.isJsonNull) null else element.asJsonObject +} + +private fun JsonObject.getAsJsonArrayOrEmpty(name: String): JsonArray { + val element = get(name) ?: return JsonArray() + return if (element.isJsonNull) JsonArray() else element.asJsonArray +} + +private fun JsonObject.toStyleApiValueDoc(): DocumentationAggregateTask.StyleApiValueDoc { + return DocumentationAggregateTask.StyleApiValueDoc( + value = get("value").asString, + codeName = get("codeName").asString, + ) +} diff --git a/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/documentation/ExtractCodeSnippetsTask.kt b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/documentation/ExtractCodeSnippetsTask.kt new file mode 100644 index 0000000000..9d9ce23c8f --- /dev/null +++ b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/documentation/ExtractCodeSnippetsTask.kt @@ -0,0 +1,107 @@ +@file:Suppress( + "UndocumentedPublicClass", + "UndocumentedPublicFunction", + "UndocumentedPublicProperty", +) + +package com.sdds.plugin.themebuilder.documentation + +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.FileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.workers.WorkerExecutor +import javax.inject.Inject + +abstract class ExtractCodeSnippetsTask : DefaultTask() { + + @get:OutputDirectory + abstract val outputKotlinDir: DirectoryProperty + + @get:OutputDirectory + abstract val outputXmlDir: DirectoryProperty + + @get:OutputFile + abstract val outputMeta: RegularFileProperty + + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val kotlinSources: ConfigurableFileCollection + + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val xmlSources: ConfigurableFileCollection + + @get:Inject + abstract val executor: WorkerExecutor + + @get:Classpath + abstract val kotlinCompiler: ConfigurableFileCollection + + @get:Input + abstract val xmlNamespace: Property + + init { + kotlinSources.from(kotlinSourceTrees()) + xmlSources.from(xmlSourceTrees()) + xmlNamespace.convention("") + } + + @TaskAction + fun compile() { + val workQueue = executor.classLoaderIsolation { + classpath.from(kotlinCompiler) + } + workQueue.submit(ActionUsingKotlinCompiler::class.java) { + kotlinSources.from(this@ExtractCodeSnippetsTask.kotlinSources) + xmlSources.from(this@ExtractCodeSnippetsTask.xmlSources) + outputKotlinDir.set(this@ExtractCodeSnippetsTask.outputKotlinDir) + outputXmlDir.set(this@ExtractCodeSnippetsTask.outputXmlDir) + outputMeta.set(this@ExtractCodeSnippetsTask.outputMeta) + namespace.set(this@ExtractCodeSnippetsTask.xmlNamespace) + projectDir.set(project.layout.projectDirectory) + } + } + + private fun kotlinSourceTrees(): FileCollection { + val roots = listOf( + project.file("src/main/kotlin"), + project.file("src/main/java"), + project.file("src"), + ).filter { it.exists() } + + return project.files( + roots.map { root -> + project.fileTree(root) { + include("**/*.kt") + } + }, + ) + } + + private fun xmlSourceTrees(): FileCollection { + val roots = listOf( + project.file("src/main/res"), + project.file("src/main/resources"), + project.file("src"), + ).filter { it.exists() } + + return project.files( + roots.map { root -> + project.fileTree(root) { + include("**/*.xml") + } + }, + ) + } +} diff --git a/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/documentation/KotlinSnippetExtractorDelegate.kt b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/documentation/KotlinSnippetExtractorDelegate.kt new file mode 100644 index 0000000000..af843c4eb2 --- /dev/null +++ b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/documentation/KotlinSnippetExtractorDelegate.kt @@ -0,0 +1,230 @@ +@file:Suppress("CyclomaticComplexMethod", "ReturnCount") + +package com.sdds.plugin.themebuilder.documentation + +import org.jetbrains.kotlin.psi.KtAnnotationEntry +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.kotlin.psi.KtPsiFactory +import org.jetbrains.kotlin.psi.KtStringTemplateExpression +import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType +import java.io.File + +internal class KotlinSnippetExtractorDelegate( + private val psiFactory: KtPsiFactory, + private val snippetsDir: File, + private val projectDir: File, +) { + fun extractFromFile(file: File): List { + val text = file.readText() + val ktFile = psiFactory.createFile(file.name, text) + + val pkg = ktFile.packageFqName.asString() + val result = mutableListOf() + + ktFile.declarations + .asSequence() + .filterIsInstance() + .forEach { fn -> + extractFromFunction( + pkg = pkg, + fn = fn, + file = file, + fileText = text, + )?.let(result::add) + } + + return result + } + + private fun extractFromFunction( + pkg: String, + fn: KtNamedFunction, + file: File, + fileText: String, + ): SampleMeta? { + val docAnn = findDocSampleAnnotation(fn) ?: return null + val id = readDocSampleId(docAnn) ?: fn.name ?: return null + val funName = fn.name ?: return null + val fqName = buildFqName(pkg, funName) + + val calls = collectSnippetCalls(fn) + if (calls.isEmpty()) return null + + val blocks = mutableListOf() + var hasRegular = false + var hasComposable = false + var minStart = Int.MAX_VALUE + var maxEnd = Int.MIN_VALUE + + calls.forEach { call -> + when (call.calleeExpression?.text) { + "composableCodeSnippet" -> hasComposable = true + else -> hasRegular = true + } + + extractNormalizedBlock(call, fileText)?.let { (block, range) -> + blocks += block + minStart = kotlin.math.min(minStart, range.startOffset) + maxEnd = kotlin.math.max(maxEnd, range.endOffset) + } + } + + val normalizedSnippet = blocks + .filter { it.isNotBlank() } + .joinToString("\n\n") + .trimEnd() + + if (normalizedSnippet.isBlank()) return null + + val kind = when { + hasComposable && hasRegular -> "mixed" + hasComposable -> "composable" + else -> "regular" + } + + val snippetStartOffset = if (minStart == Int.MAX_VALUE) 0 else minStart + val snippetEndOffset = if (maxEnd == Int.MIN_VALUE) 0 else maxEnd + + val snippetRelPath = buildSnippetRelPath(pkg = pkg, id = id) + val snippetFile = File(snippetsDir, snippetRelPath) + snippetFile.parentFile.mkdirs() + snippetFile.writeText(normalizedSnippet + "\n") + + return SampleMeta( + id = id, + kind = kind, + fqName = fqName, + file = relativePath(projectDir, file), + snippetPath = snippetFile.relativeTo(snippetsDir).path, + snippetStartOffset = snippetStartOffset, + snippetEndOffset = snippetEndOffset, + ) + } + + private fun findDocSampleAnnotation(fn: KtNamedFunction): KtAnnotationEntry? { + return fn.annotationEntries.firstOrNull { it.shortName?.asString() == "DocSample" } + } + + private fun collectSnippetCalls(fn: KtNamedFunction): List { + return fn.bodyBlockExpression + ?.statements + ?.asSequence() + ?.flatMap { it.collectDescendantsOfType().asSequence() } + ?.filter { + val callee = it.calleeExpression?.text + callee == "codeSnippet" || callee == "composableCodeSnippet" + } + ?.toList() + .orEmpty() + } + + private fun extractNormalizedBlock( + call: KtCallExpression, + fileText: String, + ): Pair? { + val lambda = call.lambdaArguments.firstOrNull()?.getLambdaExpression() ?: return null + val body = lambda.bodyExpression ?: return null + val range = body.textRange + + val rawSnippet = fileText.substring(range.startOffset, range.endOffset) + val callIndentPrefix = computeCallIndentPrefix(call, fileText) + val lambdaIndentRelative = computeLambdaIndentRelative(callIndentPrefix, range, fileText) + + val normalized = normalizeIndent(rawSnippet, callIndentPrefix, lambdaIndentRelative) + val replaced = replacePlaceholderCalls(normalized) + + return replaced to range + } + + private fun computeCallIndentPrefix(call: KtCallExpression, fileText: String): String { + val callStart = call.textRange.startOffset + val lineStart = fileText.lastIndexOf('\n', startIndex = callStart).let { if (it == -1) 0 else it + 1 } + val beforeCall = fileText.substring(lineStart, callStart) + return beforeCall.takeWhile { it == ' ' || it == '\t' } + } + + private fun computeLambdaIndentRelative( + callIndentPrefix: String, + bodyRange: org.jetbrains.kotlin.com.intellij.openapi.util.TextRange, + fileText: String, + ): String { + val bodyStart = bodyRange.startOffset + val lineStart = fileText.lastIndexOf('\n', startIndex = bodyStart).let { if (it == -1) 0 else it + 1 } + val beforeBody = fileText.substring(lineStart, bodyStart) + val lambdaBodyIndentPrefix = beforeBody.takeWhile { it == ' ' || it == '\t' } + + return if (callIndentPrefix.isNotEmpty() && lambdaBodyIndentPrefix.startsWith(callIndentPrefix)) { + lambdaBodyIndentPrefix.removePrefix(callIndentPrefix) + } else { + lambdaBodyIndentPrefix + } + } + + private fun normalizeIndent( + rawSnippet: String, + callIndentPrefix: String, + lambdaIndentRelative: String, + ): String { + return rawSnippet + .lines() + .dropWhile { it.isBlank() } + .dropLastWhile { it.isBlank() } + .map { line -> + var s = line.trimEnd() + + // 1) Shift by the indentation of the call-site. + if (callIndentPrefix.isNotEmpty() && s.startsWith(callIndentPrefix)) { + s = s.removePrefix(callIndentPrefix) + } + + // 2) Shift by the indentation level inside the lambda (relative to the call-site indent). + if (lambdaIndentRelative.isNotEmpty() && s.startsWith(lambdaIndentRelative)) { + s = s.removePrefix(lambdaIndentRelative) + } + + s + } + .joinToString("\n") + } + + private fun replacePlaceholderCalls(snippet: String): String { + val re = "placeholder\\s*\\(\\s*[^,]+,\\s*\"([^\"\\\\]*(?:\\\\.[^\"\\\\]*)*)\"\\s*\\)".toRegex() + return snippet.replace(re) { m -> m.groupValues[1].trim() } + } + + private fun readDocSampleId(ann: KtAnnotationEntry): String? { + // @DocSample(id = "x") or @DocSample("x") + val args = ann.valueArguments + if (args.isEmpty()) return null + + // Named id= + val named = args.firstOrNull { it.getArgumentName()?.asName?.asString() == "id" } + val arg = named ?: args.first() + + val expr = arg.getArgumentExpression() as? KtStringTemplateExpression ?: return null + // Support only literal strings in MVP + return expr.entries.joinToString("") { it.text }.trim('"') + } + + private fun buildFqName(pkg: String, funName: String): String = + if (pkg.isBlank()) funName else "$pkg.$funName" + + private fun buildSnippetRelPath(pkg: String, id: String): String { + fun sanitizeSegment(s: String): String = s + .trim() + .replace(Regex("[^A-Za-z0-9_.-]"), "_") + + val safeId = sanitizeSegment(id) + val pkgPath = pkg.takeIf { it.isNotBlank() } + ?.split('.') + ?.joinToString(File.separator) { sanitizeSegment(it) } + ?: "" + + return if (pkgPath.isBlank()) { + "$safeId.kt" + } else { + pkgPath + File.separator + "$safeId.kt" + } + } +} diff --git a/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/documentation/SampleMeta.kt b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/documentation/SampleMeta.kt new file mode 100644 index 0000000000..3fb6701401 --- /dev/null +++ b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/documentation/SampleMeta.kt @@ -0,0 +1,14 @@ +package com.sdds.plugin.themebuilder.documentation + +import kotlinx.serialization.Serializable + +@Serializable +internal data class SampleMeta( + val id: String, + val kind: String, + val fqName: String, + val file: String, + val snippetPath: String, + val snippetStartOffset: Int, + val snippetEndOffset: Int, +) diff --git a/build-system/conventions/src/main/kotlin/tasks/docs/UnzipCodeSnippetsTask.kt b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/documentation/UnzipCodeSnippetsTask.kt similarity index 85% rename from build-system/conventions/src/main/kotlin/tasks/docs/UnzipCodeSnippetsTask.kt rename to sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/documentation/UnzipCodeSnippetsTask.kt index 0871f02eac..fa8fe04ae4 100644 --- a/build-system/conventions/src/main/kotlin/tasks/docs/UnzipCodeSnippetsTask.kt +++ b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/documentation/UnzipCodeSnippetsTask.kt @@ -1,4 +1,11 @@ -package tasks.docs +@file:Suppress( + "NestedBlockDepth", + "UndocumentedPublicClass", + "UndocumentedPublicFunction", + "UndocumentedPublicProperty", +) + +package com.sdds.plugin.themebuilder.documentation import org.gradle.api.DefaultTask import org.gradle.api.file.ConfigurableFileCollection @@ -31,13 +38,15 @@ abstract class UnzipCodeSnippetsTask : DefaultTask() { } logger.lifecycle( - "Extracted docs snippets from ${docsArtifacts.files.size} artifact(s) into ${project.relativePath(snippetsOut)}" + "Extracted docs snippets from ${docsArtifacts.files.size} artifact(s) into ${project.relativePath( + snippetsOut, + )}", ) } private fun extractFromArtifact( artifact: File, - snippetsOut: File + snippetsOut: File, ) { ZipFile(artifact).use { zip -> zip.entries().asSequence().forEach { entry -> @@ -59,4 +68,4 @@ abstract class UnzipCodeSnippetsTask : DefaultTask() { } } } -} \ No newline at end of file +} diff --git a/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/documentation/XmlSnippetExtractorDelegate.kt b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/documentation/XmlSnippetExtractorDelegate.kt new file mode 100644 index 0000000000..7932a6d65b --- /dev/null +++ b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/documentation/XmlSnippetExtractorDelegate.kt @@ -0,0 +1,148 @@ +@file:Suppress("CyclomaticComplexMethod", "NestedBlockDepth") + +package com.sdds.plugin.themebuilder.documentation + +import java.io.File + +internal class XmlSnippetExtractorDelegate( + private val snippetsDir: File, + private val projectDir: File, + private val namespace: String, +) { + + fun extractFromFile(file: File): List { + val text = file.readText() + if (!text.contains("sample-start")) return emptyList() + + val result = mutableListOf() + + val lines = text.split('\n') + var cursorOffset = 0 + + var inside = false + var currentId: String? = null + var sampleStartOffset: Int = -1 + var sampleEndOffset: Int = -1 + + val buffer = mutableListOf() + val placeholderRules = mutableListOf>() + + fun flushSample() { + val id = currentId ?: return + + val raw = buffer.joinToString("\n") + val replaced = applyPlaceholders(raw, placeholderRules) + val normalized = normalizeIndent(replaced).trimEnd() + if (normalized.isBlank()) return + + val relPath = buildSnippetRelPathForXml(id) + val outFile = File(snippetsDir, relPath) + outFile.parentFile.mkdirs() + outFile.writeText(normalized + "\n") + + result += SampleMeta( + id = id, + kind = "xml", + fqName = "xml.$id", + file = relativePath(projectDir, file), + snippetPath = outFile.relativeTo(snippetsDir).path, + snippetStartOffset = if (sampleStartOffset >= 0) sampleStartOffset else 0, + snippetEndOffset = if (sampleEndOffset >= 0) sampleEndOffset else 0, + ) + } + + for (line in lines) { + val lineWithNewlineLen = line.length + 1 // + \n + + if (!inside) { + val id = parseSampleStartId(line) + if (id != null) { + inside = true + currentId = id + buffer.clear() + placeholderRules.clear() + sampleStartOffset = cursorOffset + lineWithNewlineLen + sampleEndOffset = -1 + } + } else { + // Inside a sample block + val endId = parseSampleEndId(line) + if (endId != null) { + // end offset is the start of the end marker line + sampleEndOffset = cursorOffset + flushSample() + + inside = false + currentId = null + buffer.clear() + placeholderRules.clear() + sampleStartOffset = -1 + sampleEndOffset = -1 + } else { + parsePlaceholderRule(line)?.let { rule -> + placeholderRules += rule + } ?: run { + if (parseSampleStartId(line) == null && parseSampleEndId(line) == null) { + buffer += line + } + } + } + } + + cursorOffset += lineWithNewlineLen + } + + return result + } + + private fun parseSampleStartId(line: String): String? { + // Examples: + // + // + val re = Regex("") + return re.find(line)?.groupValues?.get(1) + } + + private fun parseSampleEndId(line: String): String? { + val re = Regex("") + return re.find(line)?.groupValues?.get(1) + } + + private fun parsePlaceholderRule(line: String): Pair? { + // Example: + // + val re = Regex("") + val m = re.find(line) ?: return null + val from = m.groupValues[1].trim() + val to = m.groupValues[2].trim() + if (from.isBlank() || to.isBlank()) return null + return from to to + } + + private fun applyPlaceholders(text: String, rules: List>): String { + var out = text + rules.forEach { (from, to) -> + out = out.replace(from, to) + } + return out + } + + private fun normalizeIndent(text: String): String { + val lines = text.lines() + val nonBlank = lines.filter { it.isNotBlank() } + if (nonBlank.isEmpty()) return "" + + val minIndent = nonBlank + .map { it.takeWhile { ch -> ch == ' ' || ch == '\t' }.length } + .minOrNull() ?: 0 + + return lines.joinToString("\n") { line -> + if (line.isBlank()) "" else line.drop(minIndent) + } + } + + private fun buildSnippetRelPathForXml(id: String): String { + val safeId = id.replace(Regex("[^a-zA-Z0-9._-]"), "_") + return "${namespace.replace(".", "/")}/$safeId.xml" + } +} diff --git a/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/sandbox/GenerateSandboxAdaptersTask.kt b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/sandbox/GenerateSandboxAdaptersTask.kt new file mode 100644 index 0000000000..9554c753e8 --- /dev/null +++ b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/sandbox/GenerateSandboxAdaptersTask.kt @@ -0,0 +1,98 @@ +@file:Suppress( + "UndocumentedPublicClass", + "UndocumentedPublicFunction", + "UndocumentedPublicProperty", +) + +package com.sdds.plugin.themebuilder.sandbox + +import com.google.gson.GsonBuilder +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import java.io.File + +enum class SandboxTarget { + COMPOSE, + XML, +} + +enum class SandboxScheme { + V1, + V2, +} + +abstract class GenerateSandboxAdaptersTask : DefaultTask() { + + @get:InputFile + abstract val configInputFile: RegularFileProperty + + @get:Input + abstract val packageName: Property + + @get:Input + abstract val themeAlias: Property + + @get:Input + abstract val target: Property + + @get:Input + abstract val scheme: Property + + @get:Input + abstract val multiplatform: Property + + @get:OutputDirectory + abstract val outputDirectory: DirectoryProperty + + init { + group = "sandbox" + } + + @TaskAction + fun generate() { + val gson = GsonBuilder().setPrettyPrinting().create() + + val configFile = configInputFile.get().asFile + .takeIf { it.exists() } + ?.readText().orEmpty() + if (configFile.isBlank()) { + logger.warn("config file is empty or do not exists") + return + } + + val config = gson.fromJson(configFile, Config::class.java) + + val pkg = packageName.orNull ?: "com.sdds.generated" + val target = target.get() ?: throw GradleException("Property target must be specified") + val themeAlias = themeAlias.get() ?: throw GradleException("Property themeAlias must be specified") + val scheme = scheme.getOrElse(SandboxScheme.V2) + val multiplatform = multiplatform.getOrElse(false) + val mainRoot = outputDirectory.get().asFile + val pkgPath = pkg.replace('.', File.separatorChar) + val packageDir = File(mainRoot, pkgPath) + if (!packageDir.exists()) packageDir.mkdirs() + + val generator = when (target) { + SandboxTarget.COMPOSE -> { + ComposeComponentsGenerator( + config = config, + packageName = pkg, + packageDir = packageDir, + scheme = scheme, + themeAlias = themeAlias, + multiplatform = multiplatform, + ) + } + SandboxTarget.XML -> XmlComponentsGenerator(config, pkg, packageDir, scheme) + } + + generator.generate() + } +} diff --git a/build-system/conventions/src/main/kotlin/tasks/integration/ComponentsGenerator.kt b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/sandbox/SandboxGenerator.kt similarity index 89% rename from build-system/conventions/src/main/kotlin/tasks/integration/ComponentsGenerator.kt rename to sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/sandbox/SandboxGenerator.kt index 09fef68c08..a989aa40a7 100644 --- a/build-system/conventions/src/main/kotlin/tasks/integration/ComponentsGenerator.kt +++ b/sdds-core/plugin_theme_builder/src/main/kotlin/com/sdds/plugin/themebuilder/sandbox/SandboxGenerator.kt @@ -1,4 +1,6 @@ -package tasks.integration +@file:Suppress("FunctionOnlyReturningConstant") + +package com.sdds.plugin.themebuilder.sandbox import org.gradle.configurationcache.extensions.capitalized import java.io.File @@ -6,7 +8,7 @@ import java.io.File internal data class Config( val name: String, val packageName: String, - val components: List + val components: List, ) internal data class Component( @@ -93,8 +95,8 @@ internal abstract class ComponentGenerator { componentTemplate, mapOf( "key" to key.techToCamelCase(), - "appearances" to appearancesBlock - ) + "appearances" to appearancesBlock, + ), ) return listOf(result) @@ -102,14 +104,14 @@ internal abstract class ComponentGenerator { private fun List.toAppearancesBlock(appearanceTemplate: String, themeName: String): String { return joinToString( - separator = ",\n " + separator = ",\n ", ) { comp -> expand( appearanceTemplate, mapOf( "styleName" to comp.styleName, - "themeName" to themeName - ) + "themeName" to themeName, + ), ) } } @@ -132,16 +134,16 @@ internal abstract class ComponentGenerator { componentTemplate, mapOf( "key" to "Tabs", - "appearances" to tabsAppearancesBlock - ) + "appearances" to tabsAppearancesBlock, + ), ) val iconTabs = expand( componentTemplate, mapOf( "key" to "IconTabs", - "appearances" to iconTabsAppearancesBlock - ) + "appearances" to iconTabsAppearancesBlock, + ), ).takeIf { iconTabsAppearancesBlock.isNotBlank() } return listOfNotNull(tabs, iconTabs) } @@ -149,14 +151,13 @@ internal abstract class ComponentGenerator { protected fun Any.loadTemplate(path: String): String { return this::class.java.classLoader.getResource(path)?.readText().orEmpty() } - } internal class ComposeComponentsGenerator( private val config: Config, private val packageName: String, private val packageDir: File, - private val scheme: Scheme, + private val scheme: SandboxScheme, private val themeAlias: String, private val multiplatform: Boolean = false, ) : ComponentGenerator() { @@ -164,23 +165,23 @@ internal class ComposeComponentsGenerator( private val themeName = config.name.toPascalCase() private val styleProviderTemplatePath = when (scheme) { - Scheme.V1 -> "ComposeStyleProviderKt_V1.txt" - Scheme.V2 -> "ComposeStyleProviderKt_V2.txt" + SandboxScheme.V1 -> "ComposeStyleProviderKt_V1.txt" + SandboxScheme.V2 -> "ComposeStyleProviderKt_V2.txt" } private val styleInstanceTemplatePath = when (scheme) { - Scheme.V1 -> "ComposeStyleInstanceKt_V1.txt" - Scheme.V2 -> "ComposeStyleInstanceKt_V2.txt" + SandboxScheme.V1 -> "ComposeStyleInstanceKt_V1.txt" + SandboxScheme.V2 -> "ComposeStyleInstanceKt_V2.txt" } private val componentInstanceTemplatePath = when (scheme) { - Scheme.V1 -> "ComposeComponentInstanceKt_V1.txt" - Scheme.V2 -> "ComposeComponentInstanceKt_V2.txt" + SandboxScheme.V1 -> "ComposeComponentInstanceKt_V1.txt" + SandboxScheme.V2 -> "ComposeComponentInstanceKt_V2.txt" } private val componentProviderTemplatePath = when (scheme) { - Scheme.V1 -> "ComposeComponentProviderKt_V1.txt" - Scheme.V2 -> "ComposeComponentProviderKt_V2.txt" + SandboxScheme.V1 -> "ComposeComponentProviderKt_V1.txt" + SandboxScheme.V2 -> "ComposeComponentProviderKt_V2.txt" } private fun expandResourceTemplate(path: String, values: Map): String { @@ -208,7 +209,6 @@ internal class ComposeComponentsGenerator( themePackageName: String, packageDir: File, ) { - val styleProviderTemplate = loadTemplate(styleProviderTemplatePath).trim() val styleInstanceTemplate = loadTemplate(styleInstanceTemplatePath) val importTemplate = loadTemplate("ComposeImportInstanceKt.txt") @@ -219,8 +219,8 @@ internal class ComposeComponentsGenerator( styleInstanceTemplate, mapOf( "variationName" to it.composeReference.toPascalCase("."), - "variationReference" to it.composeReference - ) + "variationReference" to it.composeReference, + ), ) } @@ -239,7 +239,7 @@ internal class ComposeComponentsGenerator( "themePackageName" to themePackageName, "componentPackage" to getComponentStylePackageName(component), "variation" to it.toPascalCase(), - ) + ), ) } @@ -263,7 +263,7 @@ internal class ComposeComponentsGenerator( "bindingStyleDeclaration" to bindingStyleDeclaration, "coreStyleClass" to getCoreStyleClass(component), "coreStyleClassImport" to getCoreStyleClassImport(component), - ) + ), ) val outFile = File(packageDir, "${themeName}${component.styleName}VariationsCompose.kt") @@ -282,13 +282,15 @@ internal class ComposeComponentsGenerator( return when (component.coreName) { "BasicButton", "IconButton", - "LinkButton" -> "ButtonStyle" + "LinkButton", + -> "ButtonStyle" "TextArea" -> "TextFieldStyle" "IconBadge" -> "BadgeStyle" "BottomSheet" -> "ModalBottomSheetStyle" "TabItem", - "IconTabItem" -> "TabItemStyle" + "IconTabItem", + -> "TabItemStyle" else -> "${component.coreName}Style" } @@ -423,13 +425,13 @@ internal class ComposeComponentsGenerator( private fun StyleApiParam.defaultCodeName(): String { return defaultValue?.codeName ?: values.firstOrNull()?.codeName - ?: error("Style API param `$name` has no defaultValue and no values") + ?: error("Style API param `$name` has no defaultValue and no values") } private fun createRegisterTheme( themePackageName: String, ) { - if (scheme != Scheme.V2) return + if (scheme != SandboxScheme.V2) return val registerThemeTemplate = loadTemplate( if (multiplatform) { @@ -445,7 +447,7 @@ internal class ComposeComponentsGenerator( "themeName" to themeName, "themeAliasName" to themeAlias, "themePackageName" to themePackageName, - ) + ), ) val outFile = File(packageDir, "${themeName}RegisterTheme.kt") outFile.parentFile?.mkdirs() @@ -476,8 +478,8 @@ internal class ComposeComponentsGenerator( mapOf( "packageName" to packageName, "themeName" to themeName, - "components" to componentEntries - ) + "components" to componentEntries, + ), ) val outFile = File(packageDir, "${themeName}ComposeComponents.kt") @@ -490,29 +492,29 @@ internal class XmlComponentsGenerator( private val config: Config, private val packageName: String, private val packageDir: File, - private val scheme: Scheme, + private val scheme: SandboxScheme, ) : ComponentGenerator() { private val themeName = config.name.toPascalCase() private val viewStyleProviderTemplateName: String = when (scheme) { - Scheme.V1 -> "ViewStyleProviderKt.txt" - Scheme.V2 -> "ViewStyleProviderKt_V2.txt" + SandboxScheme.V1 -> "ViewStyleProviderKt.txt" + SandboxScheme.V2 -> "ViewStyleProviderKt_V2.txt" } private val viewStyleInstanceTemplateName: String = when (scheme) { - Scheme.V1 -> "ViewStyleInstanceKt.txt" - Scheme.V2 -> "ViewStyleInstanceKt_V2.txt" + SandboxScheme.V1 -> "ViewStyleInstanceKt.txt" + SandboxScheme.V2 -> "ViewStyleInstanceKt_V2.txt" } private val viewComponentInstanceTemplateName: String = when (scheme) { - Scheme.V1 -> "ViewComponentInstanceKt.txt" - Scheme.V2 -> "ViewComponentInstanceKt_V2.txt" + SandboxScheme.V1 -> "ViewComponentInstanceKt.txt" + SandboxScheme.V2 -> "ViewComponentInstanceKt_V2.txt" } private val viewComponentProviderTemplateName: String = when (scheme) { - Scheme.V1 -> "ViewComponentProviderKt.txt" - Scheme.V2 -> "ViewComponentProviderKt_V2.txt" + SandboxScheme.V1 -> "ViewComponentProviderKt.txt" + SandboxScheme.V2 -> "ViewComponentProviderKt_V2.txt" } private val viewAppearanceInstanceTemplateName: String = "ViewAppearanceInstanceKt.txt" @@ -524,7 +526,7 @@ internal class XmlComponentsGenerator( component = it, packageName = packageName, themePackageName = config.packageName, - packageDir = packageDir + packageDir = packageDir, ) } createViewComponentProvider(themeName, config.components, packageName, packageDir) @@ -543,21 +545,23 @@ internal class XmlComponentsGenerator( val styleContent = component.variations .joinToString("\n") { expand( - styleInstanceTemplate, mapOf( + styleInstanceTemplate, + mapOf( "variationName" to it.name.toPascalCase("."), - "variationReference" to it.viewOverlayReference.replace(".", "_") - ) + "variationReference" to it.viewOverlayReference.replace(".", "_"), + ), ) } val providerContent = expand( - styleProviderTemplate, mapOf( + styleProviderTemplate, + mapOf( "packageName" to packageName, "themeName" to themeName, "styleName" to component.styleName, "themePackageName" to themePackageName, - "variations" to styleContent - ) + "variations" to styleContent, + ), ) val outFile = File(packageDir, "${themeName}${component.styleName}VariationsView.kt") @@ -594,8 +598,8 @@ internal class XmlComponentsGenerator( mapOf( "packageName" to packageName, "themeName" to themeName, - "components" to componentEntries - ) + "components" to componentEntries, + ), ) val outFile = File(packageDir, "${themeName}ViewComponents.kt") @@ -613,7 +617,6 @@ private fun String.toPascalCase(joinSeparator: String = ""): String = .joinToString("") { it.replaceFirstChar { c -> c.uppercaseChar() } } } - internal fun String.techToCamelCase(): String { val segments = split(".", "-") return segments.joinToString("") { it.capitalized() } diff --git a/build-system/conventions/src/main/resources/ComposeAppearanceInstanceKt.txt b/sdds-core/plugin_theme_builder/src/main/resources/ComposeAppearanceInstanceKt.txt similarity index 98% rename from build-system/conventions/src/main/resources/ComposeAppearanceInstanceKt.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ComposeAppearanceInstanceKt.txt index a7f12bf565..3231c52df2 100644 --- a/build-system/conventions/src/main/resources/ComposeAppearanceInstanceKt.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ComposeAppearanceInstanceKt.txt @@ -1 +1,2 @@ "${{ styleName }}" to ${{ themeName }}${{ styleName }}VariationsCompose + diff --git a/build-system/conventions/src/main/resources/ComposeBindingBooleanPropertyKt.txt b/sdds-core/plugin_theme_builder/src/main/resources/ComposeBindingBooleanPropertyKt.txt similarity index 98% rename from build-system/conventions/src/main/resources/ComposeBindingBooleanPropertyKt.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ComposeBindingBooleanPropertyKt.txt index bcfebc9d99..e23ea04d9c 100644 --- a/build-system/conventions/src/main/resources/ComposeBindingBooleanPropertyKt.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ComposeBindingBooleanPropertyKt.txt @@ -1 +1,2 @@ Property.BooleanProperty(name = "${{ name }}", value = ${{ defaultValue }}) + diff --git a/build-system/conventions/src/main/resources/ComposeBindingBooleanValueKt.txt b/sdds-core/plugin_theme_builder/src/main/resources/ComposeBindingBooleanValueKt.txt similarity index 98% rename from build-system/conventions/src/main/resources/ComposeBindingBooleanValueKt.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ComposeBindingBooleanValueKt.txt index c9afe54634..41b67ad4e4 100644 --- a/build-system/conventions/src/main/resources/ComposeBindingBooleanValueKt.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ComposeBindingBooleanValueKt.txt @@ -1 +1,2 @@ booleanBindingValue(bindings, "${{ name }}", ${{ defaultValue }}) + diff --git a/build-system/conventions/src/main/resources/ComposeBindingDeclarationKt.txt b/sdds-core/plugin_theme_builder/src/main/resources/ComposeBindingDeclarationKt.txt similarity index 98% rename from build-system/conventions/src/main/resources/ComposeBindingDeclarationKt.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ComposeBindingDeclarationKt.txt index 0df727fd31..75d37edee3 100644 --- a/build-system/conventions/src/main/resources/ComposeBindingDeclarationKt.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ComposeBindingDeclarationKt.txt @@ -2,3 +2,4 @@ override val bindings: Set> = setOf( ${{ properties }}, ) + diff --git a/build-system/conventions/src/main/resources/ComposeBindingEnumMappingKt.txt b/sdds-core/plugin_theme_builder/src/main/resources/ComposeBindingEnumMappingKt.txt similarity index 98% rename from build-system/conventions/src/main/resources/ComposeBindingEnumMappingKt.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ComposeBindingEnumMappingKt.txt index e5814b9958..82240e483f 100644 --- a/build-system/conventions/src/main/resources/ComposeBindingEnumMappingKt.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ComposeBindingEnumMappingKt.txt @@ -1 +1,2 @@ "${{ codeName }}" -> ${{ enumClassName }}.${{ codeName }} + diff --git a/build-system/conventions/src/main/resources/ComposeBindingEnumValueKt.txt b/sdds-core/plugin_theme_builder/src/main/resources/ComposeBindingEnumValueKt.txt similarity index 99% rename from build-system/conventions/src/main/resources/ComposeBindingEnumValueKt.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ComposeBindingEnumValueKt.txt index 1a1e88e308..cb8d091eb7 100644 --- a/build-system/conventions/src/main/resources/ComposeBindingEnumValueKt.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ComposeBindingEnumValueKt.txt @@ -2,3 +2,4 @@ when (bindings["${{ name }}"]?.toString()) { ${{ mappings }} else -> ${{ defaultExpression }} } + diff --git a/build-system/conventions/src/main/resources/ComposeBindingImportKt.txt b/sdds-core/plugin_theme_builder/src/main/resources/ComposeBindingImportKt.txt similarity index 95% rename from build-system/conventions/src/main/resources/ComposeBindingImportKt.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ComposeBindingImportKt.txt index fef771b93b..ee554539af 100644 --- a/build-system/conventions/src/main/resources/ComposeBindingImportKt.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ComposeBindingImportKt.txt @@ -1 +1,2 @@ import ${{ import }} + diff --git a/build-system/conventions/src/main/resources/ComposeBindingResolveParamKt.txt b/sdds-core/plugin_theme_builder/src/main/resources/ComposeBindingResolveParamKt.txt similarity index 97% rename from build-system/conventions/src/main/resources/ComposeBindingResolveParamKt.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ComposeBindingResolveParamKt.txt index 873ee33030..a3a61a711a 100644 --- a/build-system/conventions/src/main/resources/ComposeBindingResolveParamKt.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ComposeBindingResolveParamKt.txt @@ -1 +1,2 @@ ${{ name }} = ${{ valueExpression }} + diff --git a/build-system/conventions/src/main/resources/ComposeBindingResolveStyleKeyKt.txt b/sdds-core/plugin_theme_builder/src/main/resources/ComposeBindingResolveStyleKeyKt.txt similarity index 99% rename from build-system/conventions/src/main/resources/ComposeBindingResolveStyleKeyKt.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ComposeBindingResolveStyleKeyKt.txt index fa81eb2a1d..73d2a625de 100644 --- a/build-system/conventions/src/main/resources/ComposeBindingResolveStyleKeyKt.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ComposeBindingResolveStyleKeyKt.txt @@ -3,3 +3,4 @@ override fun resolveStyleKey(bindings: Map): String { ${{ params }}, ).key } + diff --git a/build-system/conventions/src/main/resources/ComposeBindingSingleChoicePropertyKt.txt b/sdds-core/plugin_theme_builder/src/main/resources/ComposeBindingSingleChoicePropertyKt.txt similarity index 99% rename from build-system/conventions/src/main/resources/ComposeBindingSingleChoicePropertyKt.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ComposeBindingSingleChoicePropertyKt.txt index 99bcb801b3..4877121539 100644 --- a/build-system/conventions/src/main/resources/ComposeBindingSingleChoicePropertyKt.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ComposeBindingSingleChoicePropertyKt.txt @@ -1 +1,2 @@ Property.SingleChoiceProperty(name = "${{ name }}", value = "${{ defaultValue }}", variants = listOf(${{ variants }})) + diff --git a/build-system/conventions/src/main/resources/ComposeComponentInstanceKt_V1.txt b/sdds-core/plugin_theme_builder/src/main/resources/ComposeComponentInstanceKt_V1.txt similarity index 98% rename from build-system/conventions/src/main/resources/ComposeComponentInstanceKt_V1.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ComposeComponentInstanceKt_V1.txt index 73c3a68424..063c861c97 100644 --- a/build-system/conventions/src/main/resources/ComposeComponentInstanceKt_V1.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ComposeComponentInstanceKt_V1.txt @@ -4,3 +4,4 @@ ComposeComponent( ${{ appearances }} ) ) + diff --git a/build-system/conventions/src/main/resources/ComposeComponentInstanceKt_V2.txt b/sdds-core/plugin_theme_builder/src/main/resources/ComposeComponentInstanceKt_V2.txt similarity index 98% rename from build-system/conventions/src/main/resources/ComposeComponentInstanceKt_V2.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ComposeComponentInstanceKt_V2.txt index 68e05d4407..32ee810a8c 100644 --- a/build-system/conventions/src/main/resources/ComposeComponentInstanceKt_V2.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ComposeComponentInstanceKt_V2.txt @@ -4,3 +4,4 @@ Component( ${{ appearances }} ) ) + diff --git a/build-system/conventions/src/main/resources/ComposeComponentProviderKt_V1.txt b/sdds-core/plugin_theme_builder/src/main/resources/ComposeComponentProviderKt_V1.txt similarity index 99% rename from build-system/conventions/src/main/resources/ComposeComponentProviderKt_V1.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ComposeComponentProviderKt_V1.txt index fca707a789..9ea56bfffb 100644 --- a/build-system/conventions/src/main/resources/ComposeComponentProviderKt_V1.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ComposeComponentProviderKt_V1.txt @@ -15,3 +15,4 @@ object ${{ themeName }}ComposeComponents : ComponentsProviderCompose() { ${{ components }} ).associateBy { it.key } } + diff --git a/build-system/conventions/src/main/resources/ComposeComponentProviderKt_V2.txt b/sdds-core/plugin_theme_builder/src/main/resources/ComposeComponentProviderKt_V2.txt similarity index 99% rename from build-system/conventions/src/main/resources/ComposeComponentProviderKt_V2.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ComposeComponentProviderKt_V2.txt index a6644101e7..b6a1ab2cd8 100644 --- a/build-system/conventions/src/main/resources/ComposeComponentProviderKt_V2.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ComposeComponentProviderKt_V2.txt @@ -21,3 +21,4 @@ object ${{ themeName }}ComposeComponents : ComponentProvider() { ${{ components }} ).associateBy { it.key } } + diff --git a/build-system/conventions/src/main/resources/ComposeImportInstanceKt.txt b/sdds-core/plugin_theme_builder/src/main/resources/ComposeImportInstanceKt.txt similarity index 81% rename from build-system/conventions/src/main/resources/ComposeImportInstanceKt.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ComposeImportInstanceKt.txt index 5f49e9e9f3..dd170e0a79 100644 --- a/build-system/conventions/src/main/resources/ComposeImportInstanceKt.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ComposeImportInstanceKt.txt @@ -1 +1 @@ -import ${{ themePackageName }}.styles.${{ componentPackage }}.${{ variation }} \ No newline at end of file +import ${{ themePackageName }}.styles.${{ componentPackage }}.${{ variation }} diff --git a/build-system/conventions/src/main/resources/ComposeRegisterThemeKt_CMP_V2.txt b/sdds-core/plugin_theme_builder/src/main/resources/ComposeRegisterThemeKt_CMP_V2.txt similarity index 99% rename from build-system/conventions/src/main/resources/ComposeRegisterThemeKt_CMP_V2.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ComposeRegisterThemeKt_CMP_V2.txt index 2f96cbeae3..2ac7e27360 100644 --- a/build-system/conventions/src/main/resources/ComposeRegisterThemeKt_CMP_V2.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ComposeRegisterThemeKt_CMP_V2.txt @@ -38,3 +38,4 @@ fun registerTheme(componentProvider: ComponentProvider = ComponentProvider.Empty ) ThemeManager.updateTheme(theme) } + diff --git a/build-system/conventions/src/main/resources/ComposeRegisterThemeKt_V2.txt b/sdds-core/plugin_theme_builder/src/main/resources/ComposeRegisterThemeKt_V2.txt similarity index 99% rename from build-system/conventions/src/main/resources/ComposeRegisterThemeKt_V2.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ComposeRegisterThemeKt_V2.txt index 7d5ed272c8..42ad07f473 100644 --- a/build-system/conventions/src/main/resources/ComposeRegisterThemeKt_V2.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ComposeRegisterThemeKt_V2.txt @@ -39,3 +39,4 @@ fun SandboxActivity.registerTheme(componentProvider: ComponentProvider = Compone ) ThemeManager.updateTheme(theme) } + diff --git a/build-system/conventions/src/main/resources/ComposeStyleInstanceKt_V1.txt b/sdds-core/plugin_theme_builder/src/main/resources/ComposeStyleInstanceKt_V1.txt similarity index 83% rename from build-system/conventions/src/main/resources/ComposeStyleInstanceKt_V1.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ComposeStyleInstanceKt_V1.txt index d1c2e96f62..fa9830af09 100644 --- a/build-system/conventions/src/main/resources/ComposeStyleInstanceKt_V1.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ComposeStyleInstanceKt_V1.txt @@ -1 +1 @@ - "${{ variationName }}" to { ${{ variationReference }}.style() }, \ No newline at end of file + "${{ variationName }}" to { ${{ variationReference }}.style() }, diff --git a/build-system/conventions/src/main/resources/ComposeStyleInstanceKt_V2.txt b/sdds-core/plugin_theme_builder/src/main/resources/ComposeStyleInstanceKt_V2.txt similarity index 64% rename from build-system/conventions/src/main/resources/ComposeStyleInstanceKt_V2.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ComposeStyleInstanceKt_V2.txt index c2bcf786e0..d1ae04576f 100644 --- a/build-system/conventions/src/main/resources/ComposeStyleInstanceKt_V2.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ComposeStyleInstanceKt_V2.txt @@ -1 +1 @@ - "${{ variationName }}" to ComposeStyleReference { ${{ variationReference }}.style() }, \ No newline at end of file + "${{ variationName }}" to ComposeStyleReference { ${{ variationReference }}.style() }, diff --git a/build-system/conventions/src/main/resources/ComposeStyleProviderKt_V1.txt b/sdds-core/plugin_theme_builder/src/main/resources/ComposeStyleProviderKt_V1.txt similarity index 99% rename from build-system/conventions/src/main/resources/ComposeStyleProviderKt_V1.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ComposeStyleProviderKt_V1.txt index 60a7565b68..5b0e46b243 100644 --- a/build-system/conventions/src/main/resources/ComposeStyleProviderKt_V1.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ComposeStyleProviderKt_V1.txt @@ -14,3 +14,4 @@ internal object ${{ themeName }}${{ styleName }}VariationsCompose : ComposeStyle ${{ variations }} ) } + diff --git a/build-system/conventions/src/main/resources/ComposeStyleProviderKt_V2.txt b/sdds-core/plugin_theme_builder/src/main/resources/ComposeStyleProviderKt_V2.txt similarity index 99% rename from build-system/conventions/src/main/resources/ComposeStyleProviderKt_V2.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ComposeStyleProviderKt_V2.txt index bea557eb8f..41cc34237b 100644 --- a/build-system/conventions/src/main/resources/ComposeStyleProviderKt_V2.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ComposeStyleProviderKt_V2.txt @@ -30,3 +30,4 @@ ${{ variations }} ${{ bindingStyleDeclaration }} } + diff --git a/build-system/conventions/src/main/resources/ViewAppearanceInstanceKt.txt b/sdds-core/plugin_theme_builder/src/main/resources/ViewAppearanceInstanceKt.txt similarity index 98% rename from build-system/conventions/src/main/resources/ViewAppearanceInstanceKt.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ViewAppearanceInstanceKt.txt index 7205a561e0..2252946797 100644 --- a/build-system/conventions/src/main/resources/ViewAppearanceInstanceKt.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ViewAppearanceInstanceKt.txt @@ -1 +1,2 @@ "${{ styleName }}" to ${{ themeName }}${{ styleName }}VariationsView + diff --git a/build-system/conventions/src/main/resources/ViewComponentInstanceKt.txt b/sdds-core/plugin_theme_builder/src/main/resources/ViewComponentInstanceKt.txt similarity index 98% rename from build-system/conventions/src/main/resources/ViewComponentInstanceKt.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ViewComponentInstanceKt.txt index 9b948b7899..e0cf2c2ad6 100644 --- a/build-system/conventions/src/main/resources/ViewComponentInstanceKt.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ViewComponentInstanceKt.txt @@ -4,3 +4,4 @@ ViewComponent( ${{ appearances }} ) ) + diff --git a/build-system/conventions/src/main/resources/ViewComponentInstanceKt_V2.txt b/sdds-core/plugin_theme_builder/src/main/resources/ViewComponentInstanceKt_V2.txt similarity index 98% rename from build-system/conventions/src/main/resources/ViewComponentInstanceKt_V2.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ViewComponentInstanceKt_V2.txt index 68e05d4407..32ee810a8c 100644 --- a/build-system/conventions/src/main/resources/ViewComponentInstanceKt_V2.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ViewComponentInstanceKt_V2.txt @@ -4,3 +4,4 @@ Component( ${{ appearances }} ) ) + diff --git a/build-system/conventions/src/main/resources/ViewComponentProviderKt.txt b/sdds-core/plugin_theme_builder/src/main/resources/ViewComponentProviderKt.txt similarity index 99% rename from build-system/conventions/src/main/resources/ViewComponentProviderKt.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ViewComponentProviderKt.txt index 3ede72302d..f1e968b7dc 100644 --- a/build-system/conventions/src/main/resources/ViewComponentProviderKt.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ViewComponentProviderKt.txt @@ -16,3 +16,4 @@ object ${{ themeName }}ViewComponents : ComponentsProviderView() { ${{ components }} ).associateBy { it.key } } + diff --git a/build-system/conventions/src/main/resources/ViewComponentProviderKt_V2.txt b/sdds-core/plugin_theme_builder/src/main/resources/ViewComponentProviderKt_V2.txt similarity index 99% rename from build-system/conventions/src/main/resources/ViewComponentProviderKt_V2.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ViewComponentProviderKt_V2.txt index b198ffa31a..756a424462 100644 --- a/build-system/conventions/src/main/resources/ViewComponentProviderKt_V2.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ViewComponentProviderKt_V2.txt @@ -22,3 +22,4 @@ object ${{ themeName }}ViewComponents : ComponentProvider() { ${{ components }} ).associateBy { it.key } } + diff --git a/build-system/conventions/src/main/resources/ViewStyleInstanceKt.txt b/sdds-core/plugin_theme_builder/src/main/resources/ViewStyleInstanceKt.txt similarity index 85% rename from build-system/conventions/src/main/resources/ViewStyleInstanceKt.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ViewStyleInstanceKt.txt index f84b5690fb..d7563373b0 100644 --- a/build-system/conventions/src/main/resources/ViewStyleInstanceKt.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ViewStyleInstanceKt.txt @@ -1 +1 @@ - "${{ variationName }}" to DsR.style.${{ variationReference }}, \ No newline at end of file + "${{ variationName }}" to DsR.style.${{ variationReference }}, diff --git a/build-system/conventions/src/main/resources/ViewStyleInstanceKt_V2.txt b/sdds-core/plugin_theme_builder/src/main/resources/ViewStyleInstanceKt_V2.txt similarity index 67% rename from build-system/conventions/src/main/resources/ViewStyleInstanceKt_V2.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ViewStyleInstanceKt_V2.txt index 2e1b2632d3..8c70fae5d3 100644 --- a/build-system/conventions/src/main/resources/ViewStyleInstanceKt_V2.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ViewStyleInstanceKt_V2.txt @@ -1 +1 @@ - "${{ variationName }}" to viewStyleReference(DsR.style.${{ variationReference }}), \ No newline at end of file + "${{ variationName }}" to viewStyleReference(DsR.style.${{ variationReference }}), diff --git a/build-system/conventions/src/main/resources/ViewStyleProviderKt.txt b/sdds-core/plugin_theme_builder/src/main/resources/ViewStyleProviderKt.txt similarity index 99% rename from build-system/conventions/src/main/resources/ViewStyleProviderKt.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ViewStyleProviderKt.txt index 662c4b9fa2..45fc65aa46 100644 --- a/build-system/conventions/src/main/resources/ViewStyleProviderKt.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ViewStyleProviderKt.txt @@ -8,4 +8,4 @@ internal object ${{ themeName }}${{ styleName }}VariationsView : ViewStyleProvid mapOf( ${{ variations }} ) -} \ No newline at end of file +} diff --git a/build-system/conventions/src/main/resources/ViewStyleProviderKt_V2.txt b/sdds-core/plugin_theme_builder/src/main/resources/ViewStyleProviderKt_V2.txt similarity index 99% rename from build-system/conventions/src/main/resources/ViewStyleProviderKt_V2.txt rename to sdds-core/plugin_theme_builder/src/main/resources/ViewStyleProviderKt_V2.txt index d7119ff67b..c499a3387a 100644 --- a/build-system/conventions/src/main/resources/ViewStyleProviderKt_V2.txt +++ b/sdds-core/plugin_theme_builder/src/main/resources/ViewStyleProviderKt_V2.txt @@ -17,4 +17,4 @@ internal object ${{ themeName }}${{ styleName }}VariationsView : AndroidViewStyl mapOf( ${{ variations }} ) -} \ No newline at end of file +} diff --git a/sdds-core/plugin_theme_builder/src/test/kotlin/com/sdds/plugin/themebuilder/DsBuilderPluginTest.kt b/sdds-core/plugin_theme_builder/src/test/kotlin/com/sdds/plugin/themebuilder/DsBuilderPluginTest.kt new file mode 100644 index 0000000000..a053c14c74 --- /dev/null +++ b/sdds-core/plugin_theme_builder/src/test/kotlin/com/sdds/plugin/themebuilder/DsBuilderPluginTest.kt @@ -0,0 +1,514 @@ +package com.sdds.plugin.themebuilder + +import com.sdds.plugin.themebuilder.documentation.DocumentationAggregateTask +import com.sdds.plugin.themebuilder.sandbox.GenerateSandboxAdaptersTask +import com.sdds.plugin.themebuilder.sandbox.SandboxScheme +import org.gradle.api.attributes.Attribute +import org.gradle.api.internal.project.ProjectInternal +import org.gradle.testfixtures.ProjectBuilder +import org.gradle.testkit.runner.GradleRunner +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class DsBuilderPluginTest { + + @get:Rule + val temporaryFolder = TemporaryFolder() + + @Test + fun `plugin registers only dsBuilder extension`() { + val project = ProjectBuilder.builder() + .withProjectDir(temporaryFolder.root) + .build() + + project.plugins.apply(DsBuilderPlugin::class.java) + + assertNotNull(project.extensions.findByName("dsBuilder")) + assertNull(project.extensions.findByName("themeBuilder")) + } + + @Test + fun `published plugin id registers dsBuilder without legacy extension`() { + temporaryFolder.root.resolve("settings.gradle.kts").writeText("") + temporaryFolder.root.resolve("build.gradle.kts").writeText( + """ + plugins { + id("io.github.salute-developers.design-system-builder") + } + + tasks.register("verifyDsl") { + doLast { + check(project.extensions.findByName("dsBuilder") != null) + check(project.extensions.findByName("themeBuilder") == null) + } + } + """.trimIndent(), + ) + + GradleRunner.create() + .withProjectDir(temporaryFolder.root) + .withPluginClasspath() + .withArguments("verifyDsl") + .build() + } + + @Test + fun `capabilities are independently activated by their blocks`() { + val project = ProjectBuilder.builder() + .withProjectDir(temporaryFolder.root) + .build() + project.plugins.apply(DsBuilderPlugin::class.java) + val extension = project.extensions.getByType(DsBuilderExtension::class.java) + + extension.documentation { compose() } + + assertTrue(extension.documentation.enabled.get()) + assertNotNull(extension.documentation.compose) + assertFalse(extension.theme.enabled.get()) + assertFalse(extension.components.enabled.get()) + assertFalse(extension.sandbox.enabled.get()) + } + + @Test + fun `root generation settings are conventions for theme and components`() { + val project = ProjectBuilder.builder() + .withProjectDir(temporaryFolder.root) + .build() + project.plugins.apply(DsBuilderPlugin::class.java) + val extension = project.extensions.getByType(DsBuilderExtension::class.java) + + extension.compose() + extension.packageName.set("com.example.shared") + extension.resourcePrefix.set("shared") + extension.outputLocation.set(OutputLocation.SRC) + extension.autoGenerate.set(false) + extension.dimensions { + fromResources(true) + multiplier(2f) + } + + listOf(extension.theme, extension.components).forEach { capability -> + assertEquals(setOf(DsBuilderPlatform.COMPOSE), capability.targets.get()) + assertEquals("com.example.shared", capability.packageName.get()) + assertEquals("shared", capability.resourcesPrefix.get()) + assertEquals(OutputLocation.SRC, capability.outputLocation.get()) + assertTrue(capability.dimensions.get().fromResources) + assertEquals(2f, capability.dimensions.get().multiplier) + } + assertEquals(OutputLocation.SRC, extension.sandbox.outputLocation.get()) + listOf( + extension.theme, + extension.components, + extension.documentation, + extension.sandbox, + ).forEach { capability -> + assertFalse(capability.autoGenerate.get()) + } + } + + @Test + fun `capability generation setting overrides root convention`() { + val project = ProjectBuilder.builder() + .withProjectDir(temporaryFolder.root) + .build() + project.plugins.apply(DsBuilderPlugin::class.java) + val extension = project.extensions.getByType(DsBuilderExtension::class.java) + + extension.packageName.set("com.example.shared") + extension.components.packageName.set("com.example.components") + + assertEquals("com.example.shared", extension.theme.packageName.get()) + assertEquals("com.example.components", extension.components.packageName.get()) + } + + @Test + fun `sandbox scheme defaults to V2`() { + val project = ProjectBuilder.builder() + .withProjectDir(temporaryFolder.root) + .build() + project.plugins.apply(DsBuilderPlugin::class.java) + val extension = project.extensions.getByType(DsBuilderExtension::class.java) + + extension.sandbox { compose {} } + + assertEquals(SandboxScheme.V2, extension.sandbox.compose?.scheme?.get()) + } + + @Test + fun `sdds directory convention resolves current project`() { + temporaryFolder.root.resolve(".sdds").mkdir() + val project = ProjectBuilder.builder() + .withProjectDir(temporaryFolder.root) + .build() + project.plugins.apply(DsBuilderPlugin::class.java) + + val extension = project.extensions.getByType(DsBuilderExtension::class.java) + + assertTrue( + extension.sddsDirectory.get().asFile.canonicalFile == + temporaryFolder.root.resolve(".sdds").canonicalFile, + ) + } + + @Test + fun `sdds directory convention resolves Gradle parent project`() { + val parentDirectory = temporaryFolder.newFolder("parent") + val childDirectory = parentDirectory.resolve("child").apply { mkdir() } + val parentSdds = parentDirectory.resolve(".sdds").apply { mkdir() } + val parent = ProjectBuilder.builder() + .withProjectDir(parentDirectory) + .build() + val child = ProjectBuilder.builder() + .withName("child") + .withProjectDir(childDirectory) + .withParent(parent) + .build() + child.plugins.apply(DsBuilderPlugin::class.java) + + val extension = child.extensions.getByType(DsBuilderExtension::class.java) + + assertEquals(parentSdds.canonicalFile, extension.sddsDirectory.get().asFile.canonicalFile) + } + + @Test + fun `platform info conventions are derived from shared sdds directory`() { + val sdds = temporaryFolder.root.resolve("metadata").apply { mkdir() } + val project = ProjectBuilder.builder() + .withProjectDir(temporaryFolder.root) + .build() + project.plugins.apply(DsBuilderPlugin::class.java) + val extension = project.extensions.getByType(DsBuilderExtension::class.java) + extension.sddsDirectory.set(sdds) + + extension.documentation { + compose() + view() + } + extension.sandbox { + compose {} + view {} + } + + assertEquals(sdds.resolve("config.json"), extension.configFile.get().asFile) + assertEquals( + sdds.resolve("config-info-compose.json"), + extension.documentation.compose?.componentsInfoFile?.get()?.asFile, + ) + assertEquals( + sdds.resolve("theme-info-compose.json"), + extension.documentation.compose?.themeInfoFile?.get()?.asFile, + ) + assertEquals( + sdds.resolve("config-info-view-system.json"), + extension.documentation.view?.componentsInfoFile?.get()?.asFile, + ) + assertEquals( + sdds.resolve("theme-info-view-system.json"), + extension.documentation.view?.themeInfoFile?.get()?.asFile, + ) + assertEquals( + sdds.resolve("config-info-compose.json"), + extension.sandbox.compose?.componentsInfoFile?.get()?.asFile, + ) + assertEquals( + sdds.resolve("config-info-view-system.json"), + extension.sandbox.view?.componentsInfoFile?.get()?.asFile, + ) + } + + @Test + fun `explicit platform info overrides standard convention`() { + val sdds = temporaryFolder.root.resolve(".sdds").apply { mkdir() } + val override = temporaryFolder.root.resolve("custom-components.json") + val project = ProjectBuilder.builder() + .withProjectDir(temporaryFolder.root) + .build() + project.plugins.apply(DsBuilderPlugin::class.java) + val extension = project.extensions.getByType(DsBuilderExtension::class.java) + extension.sddsDirectory.set(sdds) + + extension.documentation { + compose { + componentsInfoFile.set(override) + } + } + + assertEquals( + override, + extension.documentation.compose?.componentsInfoFile?.get()?.asFile, + ) + } + + @Test + fun `theme capability preserves default and additional tenant variations`() { + val projectDir = temporaryFolder.root + projectDir.resolve(".sdds/config.json").apply { + parentFile.mkdirs() + writeText( + """ + { + "tenants": [ + { "name": "base", "alias": "Theme" }, + { "name": "business", "alias": "Business" } + ] + } + """.trimIndent(), + ) + } + createTenantFiles(projectDir.resolve(".sdds/base")) + createTenantFiles(projectDir.resolve(".sdds/business")) + projectDir.resolve(".sdds/tenants/palette.json").apply { + parentFile.mkdirs() + writeText("{}") + } + val project = ProjectBuilder.builder() + .withProjectDir(projectDir) + .build() + project.plugins.apply(DsBuilderPlugin::class.java) + project.extensions.getByType(DsBuilderExtension::class.java).theme { + compose() + autoGenerate.set(false) + } + + (project as ProjectInternal).evaluate() + + val task = project.tasks.getByName("generateTheme") as GenerateThemeTask + assertEquals(listOf("", "Business"), task.themeTenants.get()) + assertEquals("Theme", task.themeName.get()) + assertNull(project.tasks.findByName("generateComponents")) + } + + @Test + fun `documentation capability registers local aggregation without portal tasks`() { + val projectDir = temporaryFolder.root + projectDir.resolve(".sdds").mkdir() + val project = ProjectBuilder.builder().withProjectDir(projectDir).build() + val preBuild = project.tasks.register("preBuild") + project.plugins.apply(DsBuilderPlugin::class.java) + project.extensions.getByType(DsBuilderExtension::class.java).documentation { + compose() + } + + (project as ProjectInternal).evaluate() + + assertNotNull(project.configurations.findByName("sddsCoreDocumentation")) + assertEquals( + "templates", + project.configurations.getByName("sddsCoreDocumentation").attributes.getAttribute( + Attribute.of("com.sdds.docs.variant", String::class.java), + ), + ) + assertTrue( + preBuild.get().taskDependencies.getDependencies(preBuild.get()) + .any { it.name == "documentationAggregate" }, + ) + assertNotNull(project.tasks.findByName("documentationExtract")) + val aggregate = project.tasks.getByName("documentationAggregate") as DocumentationAggregateTask + assertEquals( + projectDir.resolve("override-docs").canonicalFile, + aggregate.userDocumentationRoot.get().asFile.canonicalFile, + ) + assertNull(project.tasks.findByName("docusaurusGenerate")) + assertNull(project.tasks.findByName("npmInstall")) + assertNull(project.tasks.findByName("publishDocumentation")) + } + + @Test + fun `compose sandbox derives package theme alias and generated output`() { + val projectDir = temporaryFolder.root + createSandboxMetadata(projectDir, "config-info-compose.json") + val project = ProjectBuilder.builder().withProjectDir(projectDir).build() + val preBuild = project.tasks.register("preBuild") + project.plugins.apply(DsBuilderPlugin::class.java) + val extension = project.extensions.getByType(DsBuilderExtension::class.java) + extension.sandbox { + compose {} + } + + (project as ProjectInternal).evaluate() + + val task = project.tasks.getByName("generateComposeSandbox") as GenerateSandboxAdaptersTask + assertEquals("com.example.theme.sandbox", task.packageName.get()) + assertEquals("BaseTheme", task.themeAlias.get()) + assertEquals( + project.layout.buildDirectory.dir("generated/sdds/sandbox").get().asFile, + task.outputDirectory.get().asFile, + ) + assertTrue( + preBuild.get().taskDependencies.getDependencies(preBuild.get()) + .any { it.name == "generateComposeSandbox" }, + ) + assertTrue( + ComposeSandboxPlatform::class.java.methods.none { + it.name.contains("tenant", ignoreCase = true) + }, + ) + } + + @Test + fun `view sandbox registers independently from compose sandbox`() { + val projectDir = temporaryFolder.root + createSandboxMetadata(projectDir, "config-info-view-system.json") + val project = ProjectBuilder.builder().withProjectDir(projectDir).build() + project.plugins.apply(DsBuilderPlugin::class.java) + project.extensions.getByType(DsBuilderExtension::class.java).sandbox { + view { + generatedPackageName.set("com.example.explicit") + themeAlias.set("ExplicitTheme") + } + } + + (project as ProjectInternal).evaluate() + + val task = project.tasks.getByName("generateViewSandbox") as GenerateSandboxAdaptersTask + assertEquals("com.example.explicit", task.packageName.get()) + assertEquals("ExplicitTheme", task.themeAlias.get()) + assertNull(project.tasks.findByName("generateComposeSandbox")) + } + + @Test + fun `sandbox output location can override root convention`() { + val projectDir = temporaryFolder.newFolder("sandbox-src") + createSandboxMetadata(projectDir, "config-info-compose.json") + val project = ProjectBuilder.builder().withProjectDir(projectDir).build() + val preBuild = project.tasks.register("preBuild") + project.plugins.apply(DsBuilderPlugin::class.java) + project.extensions.getByType(DsBuilderExtension::class.java).apply { + outputLocation.set(OutputLocation.BUILD) + sandbox { + outputLocation.set(OutputLocation.SRC) + autoGenerate.set(false) + compose {} + } + } + + (project as ProjectInternal).evaluate() + + val task = project.tasks.getByName("generateComposeSandbox") as GenerateSandboxAdaptersTask + assertEquals( + projectDir.resolve("src/main/kotlin").canonicalFile, + task.outputDirectory.get().asFile.canonicalFile, + ) + assertTrue( + preBuild.get().taskDependencies.getDependencies(preBuild.get()) + .none { it.name == "generateComposeSandbox" }, + ) + } + + @Test + fun `components auto generation is attached to preBuild`() { + val project = ProjectBuilder.builder() + .withProjectDir(temporaryFolder.newFolder("components-auto")) + .build() + val preBuild = project.tasks.register("preBuild") + project.configurations.create("compileClasspath") + project.plugins.apply(DsBuilderPlugin::class.java) + project.extensions.getByType(DsBuilderExtension::class.java).components { + compose() + source("https://example.com/components.zip") + } + + (project as ProjectInternal).evaluate() + + assertTrue( + preBuild.get().taskDependencies.getDependencies(preBuild.get()) + .any { it.name == "generateComponents" }, + ) + } + + @Test + fun `multiplatform sandbox generates into wired common source set`() { + val projectDir = temporaryFolder.newFolder("kmp-sandbox") + createSandboxMetadata(projectDir, "config-info-compose.json") + projectDir.resolve("settings.gradle.kts").writeText("") + projectDir.resolve("build.gradle.kts").writeText( + """ + import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension + + plugins { + id("org.jetbrains.kotlin.multiplatform") + id("io.github.salute-developers.design-system-builder") + } + + kotlin { + jvm() + } + + dsBuilder { + outputLocation.set(com.sdds.plugin.themebuilder.OutputLocation.SRC) + sandbox { + compose { + multiplatform.set(true) + } + } + } + + tasks.register("verifySandboxWiring") { + dependsOn("generateComposeSandbox") + doLast { + val expected = layout.projectDirectory.dir("src/commonMain/kotlin").asFile + check(expected.walkTopDown().any { it.extension == "kt" }) + val commonMain = project.extensions + .getByType(KotlinMultiplatformExtension::class.java) + .sourceSets + .getByName("commonMain") + check(commonMain.kotlin.srcDirs.any { it.canonicalFile == expected.canonicalFile }) + } + } + """.trimIndent(), + ) + + GradleRunner.create() + .withProjectDir(projectDir) + .withPluginClasspath() + .withArguments("verifySandboxWiring") + .build() + } + + private fun createTenantFiles(directory: java.io.File) { + directory.resolve("android").mkdirs() + directory.resolve("meta.json").writeText("{}") + listOf( + "android_color.json", + "android_gradient.json", + "android_typography.json", + "android_fontFamily.json", + "android_shape.json", + "android_shadow.json", + "android_spacing.json", + ).forEach { + directory.resolve("android/$it").writeText("{}") + } + } + + private fun createSandboxMetadata(projectDir: java.io.File, infoName: String) { + projectDir.resolve(".sdds/$infoName").apply { + parentFile.mkdirs() + writeText( + """ + { + "name": "Example", + "packageName": "com.example.theme", + "components": [] + } + """.trimIndent(), + ) + } + projectDir.resolve(".sdds/config.json").writeText( + """ + { + "tenants": [ + { "name": "base", "alias": "BaseTheme" }, + { "name": "business", "alias": "BusinessTheme" } + ] + } + """.trimIndent(), + ) + } +} diff --git a/sdds-core/plugin_theme_builder/src/test/kotlin/com/sdds/plugin/themebuilder/SddsDirectoryResolverTest.kt b/sdds-core/plugin_theme_builder/src/test/kotlin/com/sdds/plugin/themebuilder/SddsDirectoryResolverTest.kt new file mode 100644 index 0000000000..421184d41f --- /dev/null +++ b/sdds-core/plugin_theme_builder/src/test/kotlin/com/sdds/plugin/themebuilder/SddsDirectoryResolverTest.kt @@ -0,0 +1,64 @@ +package com.sdds.plugin.themebuilder + +import org.gradle.api.GradleException +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class SddsDirectoryResolverTest { + + @get:Rule + val temporaryFolder = TemporaryFolder() + + @Test + fun `current project wins over parent project`() { + val parent = temporaryFolder.newFolder("parent") + val child = parent.resolve("child").apply { mkdir() } + val parentSdds = parent.resolve(".sdds").apply { mkdir() } + val childSdds = child.resolve(".sdds").apply { mkdir() } + + val resolved = SddsDirectoryResolver(child, parent).resolve() + + assertEquals(childSdds.canonicalFile, resolved.canonicalFile) + assertTrue(resolved.canonicalFile != parentSdds.canonicalFile) + } + + @Test + fun `parent project is used when current project has no sdds`() { + val parent = temporaryFolder.newFolder("parent") + val child = parent.resolve("child").apply { mkdir() } + val parentSdds = parent.resolve(".sdds").apply { mkdir() } + + val resolved = SddsDirectoryResolver(child, parent).resolve() + + assertEquals(parentSdds.canonicalFile, resolved.canonicalFile) + } + + @Test + fun `platform paths follow standard conventions`() { + val project = temporaryFolder.newFolder("project") + val sdds = project.resolve(".sdds").apply { mkdir() } + val components = sdds.resolve("config-info-compose.json").apply { writeText("{}") } + val theme = sdds.resolve("theme-info-compose.json").apply { writeText("{}") } + val resolver = SddsDirectoryResolver(project) + + assertEquals(components, resolver.componentsInfoFile(DsBuilderPlatform.COMPOSE)) + assertEquals(theme, resolver.themeInfoFile(DsBuilderPlatform.COMPOSE)) + } + + @Test + fun `missing directory error lists checked paths`() { + val parent = temporaryFolder.newFolder("parent") + val child = parent.resolve("child").apply { mkdir() } + + val error = runCatching { + SddsDirectoryResolver(child, parent).resolve() + }.exceptionOrNull() + + assertTrue(error is GradleException) + assertTrue(error?.message.orEmpty().contains(child.resolve(".sdds").absolutePath)) + assertTrue(error?.message.orEmpty().contains(parent.resolve(".sdds").absolutePath)) + } +} diff --git a/sdds-core/plugin_theme_builder/src/test/kotlin/com/sdds/plugin/themebuilder/ThemeBuilderPluginTest.kt b/sdds-core/plugin_theme_builder/src/test/kotlin/com/sdds/plugin/themebuilder/ThemeBuilderPluginTest.kt index 90e89946ec..dc0a8fc8c0 100644 --- a/sdds-core/plugin_theme_builder/src/test/kotlin/com/sdds/plugin/themebuilder/ThemeBuilderPluginTest.kt +++ b/sdds-core/plugin_theme_builder/src/test/kotlin/com/sdds/plugin/themebuilder/ThemeBuilderPluginTest.kt @@ -22,10 +22,12 @@ class ThemeBuilderPluginTest { .withProjectDir(projectDir) .build() project.configurations.create("compileClasspath") - project.plugins.apply(ThemeBuilderPlugin::class.java) - project.extensions.getByType(ThemeBuilderExtension::class.java).apply { - compose() - autoGenerate(false) + project.plugins.apply(DsBuilderPlugin::class.java) + project.extensions.getByType(DsBuilderExtension::class.java).apply { + theme { + compose() + autoGenerate.set(false) + } } (project as ProjectInternal).evaluate() diff --git a/sdds-core/plugin_theme_builder/src/test/kotlin/com/sdds/plugin/themebuilder/documentation/DocumentationAggregateTaskTest.kt b/sdds-core/plugin_theme_builder/src/test/kotlin/com/sdds/plugin/themebuilder/documentation/DocumentationAggregateTaskTest.kt new file mode 100644 index 0000000000..0b9ea482dd --- /dev/null +++ b/sdds-core/plugin_theme_builder/src/test/kotlin/com/sdds/plugin/themebuilder/documentation/DocumentationAggregateTaskTest.kt @@ -0,0 +1,490 @@ +package com.sdds.plugin.themebuilder.documentation + +import org.gradle.api.GradleException +import org.gradle.testfixtures.ProjectBuilder +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +class DocumentationAggregateTaskTest { + + @get:Rule + val temporaryFolder = TemporaryFolder() + + @Test + fun `aggregation follows ADR layout and local snippets override core artifacts`() { + val project = ProjectBuilder.builder().withProjectDir(temporaryFolder.root).build() + val task = project.tasks.create("aggregate", DocumentationAggregateTask::class.java) + val first = createJar("a.jar", "META-INF/sdds-docs/assets/examples/kotlin/sample.kt", "first") + val second = createJar("b.jar", "META-INF/sdds-docs/assets/examples/kotlin/sample.kt", "first") + val kotlin = temporaryFolder.newFolder("kotlin").apply { + resolve("sample.kt").writeText("local") + } + val xml = temporaryFolder.newFolder("xml").apply { + resolve("sample.xml").writeText("") + } + val samples = temporaryFolder.newFile("samples.json").apply { writeText("[]") } + val components = temporaryFolder.newFile("components.json").apply { writeText("{}") } + val theme = temporaryFolder.newFile("theme.json").apply { writeText("{}") } + val output = temporaryFolder.root.resolve("output") + task.coreArtifacts.from(second, first) + task.kotlinSnippets.set(kotlin) + task.xmlSnippets.set(xml) + task.samplesMetadata.set(samples) + task.componentsInfoFile.set(components) + task.themeInfoFile.set(theme) + task.outputDirectory.set(output) + + task.aggregate() + + assertEquals("local", output.resolve("assets/examples/kotlin/sample.kt").readText()) + assertEquals("", output.resolve("assets/examples/xml/sample.xml").readText()) + assertEquals("[]", output.resolve("meta/samples.json").readText()) + assertEquals("{}", output.resolve("meta/components-info.json").readText()) + assertEquals("{}", output.resolve("meta/theme-info.json").readText()) + } + + @Test + fun `aggregation accepts empty core artifacts`() { + val task = configuredTask() + + task.aggregate() + + assertTrue(task.outputDirectory.get().asFile.resolve("assets/examples/kotlin").isDirectory) + assertTrue(task.outputDirectory.get().asFile.resolve("assets/examples/xml").isDirectory) + assertTrue(task.outputDirectory.get().asFile.resolve("meta/components-info.json").isFile) + } + + @Test + fun `legacy core artifact entries are normalized to ADR kotlin examples directory`() { + val task = configuredTask() + task.coreArtifacts.from( + createJar( + "legacy.jar", + "META-INF/sdds-docs/com/sdds/Sample.kt", + "legacy", + ), + ) + + task.aggregate() + + assertEquals( + "legacy", + task.outputDirectory.get().asFile + .resolve("assets/examples/kotlin/com/sdds/Sample.kt") + .readText(), + ) + } + + @Test + fun `legacy core meta is merged into samples metadata and not copied as a snippet`() { + val task = configuredTask() + task.coreArtifacts.from( + createJar( + "core-meta.jar", + "META-INF/sdds-docs/meta.json", + """[{"id":"CoreSample","kind":"composable","snippetPath":"com/sdds/CoreSample.kt"}]""", + ), + ) + + task.aggregate() + + val output = task.outputDirectory.get().asFile + val metadata = output.resolve("meta/samples.json").readText() + assertTrue(metadata.contains("\"id\": \"CoreSample\"")) + assertTrue(metadata.contains("\"snippetPath\": \"assets/examples/kotlin/com/sdds/CoreSample.kt\"")) + assertTrue(!output.resolve("assets/examples/kotlin/meta.json").exists()) + } + + @Test + fun `xml snippet path is relative to documentation root`() { + val task = configuredTask() + task.samplesMetadata.set( + temporaryFolder.newFile("xml-meta.json").apply { + writeText("""[{"id":"XmlSample","kind":"xml","snippetPath":"XmlSample.xml"}]""") + }, + ) + + task.aggregate() + + val metadata = task.outputDirectory.get().asFile.resolve("meta/samples.json").readText() + assertTrue(metadata.contains("\"snippetPath\": \"assets/examples/xml/XmlSample.xml\"")) + } + + @Test + fun `local sample metadata overrides core sample with the same id`() { + val task = configuredTask() + task.coreArtifacts.from( + createJar( + "core-meta.jar", + "META-INF/sdds-docs/meta.json", + """[{"id":"Sample","kind":"core"}]""", + ), + ) + task.samplesMetadata.set( + temporaryFolder.newFile("local-meta.json").apply { + writeText("""[{"id":"Sample","kind":"local"}]""") + }, + ) + + task.aggregate() + + val metadata = task.outputDirectory.get().asFile.resolve("meta/samples.json").readText() + assertTrue(metadata.contains("\"kind\": \"local\"")) + assertTrue(!metadata.contains("\"kind\": \"core\"")) + } + + @Test + fun `core structure enriches only public markdown with local samples`() { + val task = configuredTask() + val structure = """ + { + "schemaVersion": "1.0", + "navigation": [ + { + "title": "Components", + "items": [{"title": "Button", "path": "components/Button.md"}] + } + ] + } + """.trimIndent() + val markdown = """ + ```kotlin + // @sample: sample.kt + ``` + ```xml + + ``` + """.trimIndent() + task.coreArtifacts.from( + createJar( + "templates.jar", + "META-INF/sdds-docs/structure.json" to structure, + "META-INF/sdds-docs/docs/components/Button.md" to markdown, + "META-INF/sdds-docs/docs/components/Draft.md" to "draft", + "META-INF/sdds-docs/assets/examples/kotlin/sample.kt" to "core", + ), + ) + task.kotlinSnippets.get().asFile.resolve("sample.kt").writeText("local") + task.xmlSnippets.get().asFile.resolve("sample.xml").writeText("") + + task.aggregate() + + val output = task.outputDirectory.get().asFile + val content = output.resolve("content/core/components/Button.md").readText() + assertTrue(content.contains("local")) + assertTrue(content.contains("")) + assertTrue(!content.contains("@sample")) + assertTrue(!output.resolve("content/core/components/Draft.md").exists()) + assertTrue(!output.resolve("content/user").exists()) + assertTrue(!output.resolve("structure-user.json").exists()) + assertTrue(output.resolve("structure-core.json").isFile) + } + + @Test + fun `core markdown includes style api and preserves screenshot directive`() { + val task = configuredTask() + task.screenshotsDirectory.set( + temporaryFolder.newFolder("screenshots").apply { + resolve("sample.Button.Simple.png").writeBytes(byteArrayOf(1, 2, 3)) + }, + ) + task.coreArtifacts.from( + createJar( + "rich-template.jar", + "META-INF/sdds-docs/structure.json" to + """{"navigation":[{"title":"Button","path":"components/ButtonUsage.md"}]}""", + "META-INF/sdds-docs/docs/components/ButtonUsage.md" to + """ + + + """.trimIndent(), + ), + ) + task.componentsInfoFile.set( + temporaryFolder.newFile("style-components.json").apply { + writeText( + """ + { + "components": [{ + "coreName": "Button", + "styleName": "Button", + "styleApi": { + "receiverClassName": "ButtonStyles.Companion", + "params": [] + }, + "variations": [] + }] + } + """.trimIndent(), + ) + }, + ) + task.aggregate() + + val output = task.outputDirectory.get().asFile + val content = output.resolve("content/core/components/ButtonUsage.md").readText() + assertTrue(content.contains("")) + assertTrue(output.resolve("assets/screenshots/sample.Button.Simple.png").isFile) + assertTrue(content.contains("Пример выбора готового стиля")) + assertTrue(!content.contains("@style-api")) + } + + @Test + fun `missing public markdown reports its path`() { + val task = configuredTask() + task.coreArtifacts.from( + createJar( + "missing-template.jar", + "META-INF/sdds-docs/structure.json" to + """{"navigation":[{"title":"Missing","path":"missing.md"}]}""", + ), + ) + + val error = runCatching(task::aggregate).exceptionOrNull() + + assertTrue(error is GradleException) + assertTrue(error?.message.orEmpty().contains("missing.md")) + } + + @Test + fun `missing sample reports template and reference`() { + val task = configuredTask() + task.coreArtifacts.from( + createJar( + "missing-sample.jar", + "META-INF/sdds-docs/structure.json" to + """{"navigation":[{"title":"Page","path":"page.md"}]}""", + "META-INF/sdds-docs/docs/page.md" to "// @sample: Missing.kt", + ), + ) + + val error = runCatching(task::aggregate).exceptionOrNull() + + assertTrue(error is GradleException) + assertTrue(error?.message.orEmpty().contains("core page 'page.md'")) + assertTrue(error?.message.orEmpty().contains("Missing.kt")) + } + + @Test + fun `user standalone page is enriched and unlisted draft is ignored`() { + val task = configuredTask() + val user = temporaryFolder.newFolder("user") + user.resolve("structure.json").writeText( + """{"navigation":[{"title":"Custom","path":"components/Custom.md","hidden":true}]}""", + ) + user.resolve("docs/components").mkdirs() + user.resolve("docs/components/Custom.md").writeText("// @sample: sample.kt") + user.resolve("docs/components/Draft.md").writeText("draft") + task.kotlinSnippets.get().asFile.resolve("sample.kt").writeText("user sample") + task.userDocumentationRoot.set(user) + + task.aggregate() + + val output = task.outputDirectory.get().asFile + assertEquals("user sample", output.resolve("content/user/components/Custom.md").readText()) + assertTrue(!output.resolve("content/user/components/Draft.md").exists()) + assertTrue(output.resolve("structure-user.json").readText().contains("\"hidden\": true")) + } + + @Test + fun `append and replace resolve legacy physical sources without merging core`() { + val task = configuredTask() + task.coreArtifacts.from( + createJar( + "core.jar", + "META-INF/sdds-docs/structure.json" to + """{"navigation":[{"path":"components/Page.md"},{"path":"components/Other.md"}]}""", + "META-INF/sdds-docs/docs/components/Page.md" to "core page", + "META-INF/sdds-docs/docs/components/Other.md" to "core other", + ), + ) + val user = temporaryFolder.newFolder("user-merge") + user.resolve("structure.json").writeText( + """ + {"navigation":[ + {"path":"components/Page.md","merge":"append"}, + {"path":"components/Other.md","merge":"replace"} + ]} + """.trimIndent(), + ) + user.resolve("docs/components").mkdirs() + user.resolve("docs/components/+Page.md").writeText("user append") + user.resolve("docs/components/Other.md").writeText("user replace") + task.userDocumentationRoot.set(user) + + task.aggregate() + + val output = task.outputDirectory.get().asFile + assertEquals("core page", output.resolve("content/core/components/Page.md").readText()) + assertEquals("user append", output.resolve("content/user/components/Page.md").readText()) + assertEquals("user replace", output.resolve("content/user/components/Other.md").readText()) + } + + @Test + fun `user style api is enriched and screenshot directive is preserved`() { + val task = configuredTask() + val user = temporaryFolder.newFolder("user-style") + user.resolve("structure.json").writeText( + """{"navigation":[{"path":"components/ButtonUsage.md"}]}""", + ) + user.resolve("docs/components").mkdirs() + user.resolve("docs/components/ButtonUsage.md").writeText( + "\n", + ) + task.componentsInfoFile.set( + temporaryFolder.newFile("user-style-components.json").apply { + writeText( + """ + {"components":[{"coreName":"Button","styleName":"Button", + "styleApi":{"receiverClassName":"ButtonStyles.Companion","params":[]},"variations":[]}]} + """.trimIndent(), + ) + }, + ) + task.userDocumentationRoot.set(user) + + task.aggregate() + + val content = task.outputDirectory.get().asFile + .resolve("content/user/components/ButtonUsage.md").readText() + assertTrue(content.contains("")) + assertTrue(content.contains("Пример выбора готового стиля")) + } + + @Test + fun `invalid user mappings fail with actionable diagnostics`() { + val cases = listOf( + Triple("""{"path":"page.md"}""", "page.md", "requires explicit"), + Triple("""{"path":"page.md","merge":"append"}""", "page.md", "must use source"), + Triple("""{"path":"new.md"}""", "+new.md", "must not use plus-prefixed"), + Triple("""{"path":"page.md","merge":"prepend"}""", "+page.md", "unsupported merge"), + Triple("""{"path":"../unsafe.md"}""", "../unsafe.md", "invalid relative path"), + ) + cases.forEachIndexed { index, (node, sourcePath, expected) -> + val task = configuredTask() + task.coreArtifacts.from( + createJar( + "mapping-$index.jar", + "META-INF/sdds-docs/structure.json" to + """{"navigation":[{"path":"page.md"}]}""", + "META-INF/sdds-docs/docs/page.md" to "core", + ), + ) + val user = temporaryFolder.newFolder("invalid-user-$index") + user.resolve("structure.json").writeText("""{"navigation":[$node]}""") + user.resolve("docs").mkdirs() + user.resolve("docs/$sourcePath").apply { + parentFile.mkdirs() + writeText("user") + } + task.userDocumentationRoot.set(user) + + val error = runCatching(task::aggregate).exceptionOrNull() + + assertTrue("Expected '$expected' for case $index: ${error?.message}", error is GradleException) + assertTrue(error?.message.orEmpty().contains(expected)) + } + } + + @Test + fun `missing user source reports logical path and user sample reports layer`() { + val task = configuredTask() + val user = temporaryFolder.newFolder("missing-user") + user.resolve("structure.json").writeText("""{"navigation":[{"path":"missing.md"}]}""") + task.userDocumentationRoot.set(user) + val missingSource = runCatching(task::aggregate).exceptionOrNull() + assertTrue(missingSource?.message.orEmpty().contains("missing.md")) + + user.resolve("docs").mkdirs() + user.resolve("docs/missing.md").writeText("// @sample: Missing.kt") + val missingSample = runCatching(task::aggregate).exceptionOrNull() + assertTrue(missingSample?.message.orEmpty().contains("user page 'missing.md'")) + assertTrue(missingSample?.message.orEmpty().contains("Missing.kt")) + } + + @Test + fun `conflicting core templates fail aggregation`() { + val task = configuredTask() + task.coreArtifacts.from( + createJar("first-template.jar", "META-INF/sdds-docs/docs/page.md", "first"), + createJar("second-template.jar", "META-INF/sdds-docs/docs/page.md", "second"), + ) + + val error = runCatching(task::aggregate).exceptionOrNull() + + assertTrue(error is GradleException) + assertTrue(error?.message.orEmpty().contains("Conflicting Core documentation template")) + } + + @Test + fun `conflicting core assets fail aggregation`() { + val task = configuredTask() + task.coreArtifacts.from( + createJar("first-asset.jar", "META-INF/sdds-docs/assets/examples/kotlin/sample.kt", "first"), + createJar("second-asset.jar", "META-INF/sdds-docs/assets/examples/kotlin/sample.kt", "second"), + ) + + val error = runCatching(task::aggregate).exceptionOrNull() + + assertTrue(error is GradleException) + assertTrue(error?.message.orEmpty().contains("Conflicting documentation asset")) + } + + @Test + fun `unsafe core entry fails aggregation`() { + val task = configuredTask() + task.coreArtifacts.from( + createJar("unsafe.jar", "META-INF/sdds-docs/../../outside.md", "unsafe"), + ) + + val error = runCatching(task::aggregate).exceptionOrNull() + + assertTrue(error is GradleException) + assertTrue(error?.message.orEmpty().contains("invalid relative path")) + } + + @Test + fun `missing info error contains exact resolved path`() { + val missing = temporaryFolder.root.resolve("missing-components.json") + val task = configuredTask().apply { + componentsInfoFile.set(missing) + } + + val error = runCatching(task::aggregate).exceptionOrNull() + + assertTrue(error is GradleException) + assertTrue(error?.message.orEmpty().contains(missing.absolutePath)) + } + + private fun configuredTask(): DocumentationAggregateTask { + val project = ProjectBuilder.builder().withProjectDir(temporaryFolder.newFolder()).build() + return project.tasks.create("aggregate", DocumentationAggregateTask::class.java).apply { + kotlinSnippets.set(temporaryFolder.newFolder()) + xmlSnippets.set(temporaryFolder.newFolder()) + samplesMetadata.set(temporaryFolder.newFile().apply { writeText("[]") }) + componentsInfoFile.set(temporaryFolder.newFile().apply { writeText("{}") }) + themeInfoFile.set(temporaryFolder.newFile().apply { writeText("{}") }) + outputDirectory.set(temporaryFolder.newFolder()) + } + } + + private fun createJar(name: String, path: String, content: String): File = + createJar(name, path to content) + + private fun createJar(name: String, vararg entries: Pair): File = + temporaryFolder.newFile(name).also { file -> + ZipOutputStream(file.outputStream()).use { output -> + entries.forEach { (path, content) -> + output.putNextEntry(ZipEntry(path)) + output.write(content.toByteArray()) + output.closeEntry() + } + } + } +} diff --git a/tokens/build.gradle.kts b/tokens/build.gradle.kts index d8139ec0ba..2ea53975f3 100644 --- a/tokens/build.gradle.kts +++ b/tokens/build.gradle.kts @@ -35,7 +35,7 @@ buildscript { /** * Gradle-таска, генерирующая модули тем в соответствии со списком тем. - * Каждый модуль содержит файл build.gradle.kts с нужной конфигурацией themeBuilder + * Каждый модуль содержит файл build.gradle.kts с нужной конфигурацией dsBuilder * и файл манифеста. */ internal abstract class GenerateThemeModulesTask @Inject constructor() : DefaultTask() { @@ -101,18 +101,20 @@ internal abstract class GenerateThemeModulesTask @Inject constructor() : Default appendLine(" id(\"convention.compose\")") appendLine(" id(\"convention.maven-publish\")") appendLine(" id(\"convention.auto-bump\")") - appendLine(" id(libs.plugins.themebuilder.get().pluginId)") + appendLine(" id(libs.plugins.dsbuilder.get().pluginId)") appendLine("}") appendLine() appendLine("android {") appendLine(" namespace = \"com.sdds.themes.${themeName.replaceSnakeByPoint()}.tokens\"") appendLine("}") appendLine() - appendLine("themeBuilder {") - appendLine(" themeSource(name = \"$themeName\", version = \"latest\")") + appendLine("dsBuilder {") appendLine(" compose()") - appendLine(" ktPackage(ktPackage = \"com.sdds.themes.${themeName.replaceSnakeByPoint()}.tokens\")") - appendLine(" resourcesPrefix(prefix = \"sdgen\")") + appendLine(" packageName.set(\"com.sdds.themes.${themeName.replaceSnakeByPoint()}.tokens\")") + appendLine(" resourcePrefix.set(\"sdgen\")") + appendLine(" theme {") + appendLine(" source(name = \"$themeName\", version = \"latest\")") + appendLine(" }") appendLine("}") appendLine() appendLine("dependencies {") @@ -126,7 +128,7 @@ internal abstract class GenerateThemeModulesTask @Inject constructor() : Default appendLine("@Suppress(\"DSL_SCOPE_VIOLATION\")") appendLine("plugins {") appendLine(" id(\"convention.android-lib\")") - appendLine(" id(libs.plugins.themebuilder.get().pluginId)") + appendLine(" id(libs.plugins.dsbuilder.get().pluginId)") appendLine(" id(\"convention.maven-publish\")") appendLine(" id(\"convention.auto-bump\")") appendLine("}") @@ -135,11 +137,13 @@ internal abstract class GenerateThemeModulesTask @Inject constructor() : Default appendLine(" namespace = \"com.sdds.themes.${themeName.replaceSnakeByPoint()}.tokens\"") appendLine("}") appendLine() - appendLine("themeBuilder {") - appendLine(" themeSource(name = \"$themeName\", version = \"latest\")") + appendLine("dsBuilder {") appendLine(" view()") - appendLine(" ktPackage(ktPackage = \"com.sdds.themes.${themeName.replaceSnakeByPoint()}.tokens\")") - appendLine(" resourcesPrefix(prefix = \"sdgen\")") + appendLine(" packageName.set(\"com.sdds.themes.${themeName.replaceSnakeByPoint()}.tokens\")") + appendLine(" resourcePrefix.set(\"sdgen\")") + appendLine(" theme {") + appendLine(" source(name = \"$themeName\", version = \"latest\")") + appendLine(" }") appendLine("}") appendLine() appendLine("dependencies {") diff --git a/tokens/gradle.properties b/tokens/gradle.properties index 2a0d7881e0..eead976e02 100644 --- a/tokens/gradle.properties +++ b/tokens/gradle.properties @@ -25,7 +25,6 @@ nexus.snapshot=false nexus.gitUrl=https://github.com/salute-developers/plasma.git nexus.websiteUrl=https://github.com/salute-developers/plasma -integration.view.scheme=V2 org.jetbrains.compose.experimental.js.enabled=true org.jetbrains.compose.experimental.jscanvas.enabled=true org.jetbrains.compose.experimental.macos.enabled=true diff --git a/tokens/plasma-stards-compose/build.gradle.kts b/tokens/plasma-stards-compose/build.gradle.kts index a72571d785..16a0494119 100644 --- a/tokens/plasma-stards-compose/build.gradle.kts +++ b/tokens/plasma-stards-compose/build.gradle.kts @@ -14,7 +14,7 @@ plugins { id("convention.maven-publish") id("convention.auto-bump") id("convention.testing-compose") - id(libs.plugins.themebuilder.get().pluginId) + id(libs.plugins.dsbuilder.get().pluginId) alias(libs.plugins.roborazzi) id("star-dimens-generator") id("convention.docusaurus") @@ -25,20 +25,12 @@ android { resourcePrefix = themeResPrefix } -themeBuilder { - themeSource { - url(themeUrl) - name(themeAlias) - } - componentSource(name = componentsName, componentsVersion, themeAlias) - compose { - componentsMetaStyleClass(true) - } - ktPackage("com.sdkit.star.designsystem") - resourcesPrefix(prefix = themeResPrefix) - outputLocation(OutputLocation.SRC) - autoGenerate(false) - mode(ThemeBuilderMode.THEME) +dsBuilder { + autoGenerate.set(false) + compose() + packageName.set("com.sdkit.star.designsystem") + resourcePrefix.set(themeResPrefix) + outputLocation.set(OutputLocation.SRC) dimensions { fromResources(true) multiplier(2f) @@ -47,6 +39,14 @@ themeBuilder { medium(560) } } + theme { + source(url = themeUrl, name = themeAlias) + mode.set(ThemeBuilderMode.THEME) + } + components { + source(name = componentsName, version = componentsVersion, alias = themeAlias) + componentsMetaStyleClass.set(true) + } } dependencies { diff --git a/tokens/plasma-stards-compose/docs/build.gradle.kts b/tokens/plasma-stards-compose/docs/build.gradle.kts index d9e7201c07..96f01dc6d1 100644 --- a/tokens/plasma-stards-compose/docs/build.gradle.kts +++ b/tokens/plasma-stards-compose/docs/build.gradle.kts @@ -1,6 +1,13 @@ +import extensions.docs.DocusaurusExtension + @Suppress("DSL_SCOPE_VIOLATION") plugins { - id("convention.documentation-compose") + id("convention.android-lib") + id("convention.integration-detekt") + id("convention.docusaurus") + id("convention.compose") + id("com.google.devtools.ksp") + id(libs.plugins.dsbuilder.get().pluginId) id("convention.testing-compose") alias(libs.plugins.roborazzi) } @@ -9,7 +16,31 @@ android { namespace = "com.sdkit.star.designsystem.compose.docs" } +ksp { + arg("packageName", "com.sdkit.star.designsystem.compose.docs") +} + +dsBuilder { + autoGenerate.set(false) + documentation { + compose() + } +} + +extensions.configure("docusaurus") { + components.set(layout.projectDirectory.file("../.sdds/config-info-compose.json")) + snippetsDir.set(layout.projectDirectory.dir("../.sdds/temp/docs/assets/examples")) +} + +tasks.named("docusaurusGenerate") { + dependsOn("documentationAggregate") +} + dependencies { + "sddsCoreDocumentation"("integration-core:uikit-compose-fixtures:unspecified:docs@jar") + implementation("sdds-core:docs") + ksp("sdds-core:docs") + testImplementation("integration-core:uikit-compose-fixtures") implementation(project(":plasma-stards-compose")) implementation(libs.sdds.uikit.compose) implementation(libs.base.androidX.compose.foundation) diff --git a/tokens/plasma-stards-compose/integration/build.gradle.kts b/tokens/plasma-stards-compose/integration/build.gradle.kts index df0cd92365..55a2c96ca5 100644 --- a/tokens/plasma-stards-compose/integration/build.gradle.kts +++ b/tokens/plasma-stards-compose/integration/build.gradle.kts @@ -1,7 +1,10 @@ +import com.sdds.plugin.themebuilder.OutputLocation + @Suppress("DSL_SCOPE_VIOLATION") plugins { id("convention.android-lib") - id("convention.integration-compose") + id("convention.integration-detekt") + id(libs.plugins.dsbuilder.get().pluginId) id("convention.compose") } @@ -9,6 +12,17 @@ android { namespace = "com.sdkit.star.designsystem.compose.integration" } +dsBuilder { + outputLocation.set(OutputLocation.SRC) + autoGenerate.set(false) + sandbox { + compose { + generatedPackageName.set("com.sdkit.star.designsystem.integration") + themeAlias.set("StarDs") + } + } +} + dependencies { implementation(project(":plasma-stards-compose")) implementation("integration-core:sandbox-core") diff --git a/tokens/plasma-stards-compose/integration/gradle.properties b/tokens/plasma-stards-compose/integration/gradle.properties deleted file mode 100644 index 5e053c9701..0000000000 --- a/tokens/plasma-stards-compose/integration/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -integration.compose.config-path=../tokens/plasma-stards-compose/.sdds/config-info-compose.json -integration.compose.package-name=com.sdkit.star.designsystem.integration -integration.compose.scheme=V2 \ No newline at end of file diff --git a/tokens/plasma-stards-view/config-info-view-system.json b/tokens/plasma-stards-view/.sdds/config-info-view-system.json similarity index 99% rename from tokens/plasma-stards-view/config-info-view-system.json rename to tokens/plasma-stards-view/.sdds/config-info-view-system.json index f3e9c55b52..afe650a4b5 100644 --- a/tokens/plasma-stards-view/config-info-view-system.json +++ b/tokens/plasma-stards-view/.sdds/config-info-view-system.json @@ -29,6 +29,28 @@ } ] }, + { + "key": "indicator", + "coreName": "Indicator", + "styleName": "Indicator", + "variations": [ + { + "name": "l", + "viewReference": "Sdkit.StarDs.Components.Indicator.L", + "viewOverlayReference": "Sdkit.StarDs.ComponentOverlays.IndicatorL" + }, + { + "name": "m", + "viewReference": "Sdkit.StarDs.Components.Indicator.M", + "viewOverlayReference": "Sdkit.StarDs.ComponentOverlays.IndicatorM" + }, + { + "name": "s", + "viewReference": "Sdkit.StarDs.Components.Indicator.S", + "viewOverlayReference": "Sdkit.StarDs.ComponentOverlays.IndicatorS" + } + ] + }, { "key": "avatar-group", "coreName": "AvatarGroup", diff --git a/tokens/plasma-stards-view/.sdds/theme-info-view-system.json b/tokens/plasma-stards-view/.sdds/theme-info-view-system.json new file mode 100644 index 0000000000..fcdcc9ab7f --- /dev/null +++ b/tokens/plasma-stards-view/.sdds/theme-info-view-system.json @@ -0,0 +1,6 @@ +{ + "name": "StarDs", + "version": "0.7.0-alpha", + "platform": "vs", + "tokens": [] +} \ No newline at end of file diff --git a/tokens/plasma-stards-view/build.gradle.kts b/tokens/plasma-stards-view/build.gradle.kts index 27f8e287d1..4c59f35cde 100644 --- a/tokens/plasma-stards-view/build.gradle.kts +++ b/tokens/plasma-stards-view/build.gradle.kts @@ -11,7 +11,7 @@ import utils.themeResPrefix @Suppress("DSL_SCOPE_VIOLATION") plugins { id("convention.android-lib") - id(libs.plugins.themebuilder.get().pluginId) + id(libs.plugins.dsbuilder.get().pluginId) id("convention.maven-publish") id("convention.auto-bump") id("convention.testing") @@ -25,12 +25,8 @@ android { resourcePrefix = themeResPrefix } -themeBuilder { - themeSource { - url(themeUrl) - name(themeAlias) - } - componentSource(name = componentsName, componentsVersion, themeAlias) +dsBuilder { + autoGenerate.set(false) view { themeParents { materialComponentsTheme("NoActionBar") @@ -38,11 +34,9 @@ themeBuilder { } setupShapeAppearance(sddsShape()) } - ktPackage("com.sdkit.star.designsystem") - resourcesPrefix(prefix = themeResPrefix) - outputLocation(OutputLocation.SRC) - autoGenerate(false) - mode(ThemeBuilderMode.THEME) + packageName.set("com.sdkit.star.designsystem") + resourcePrefix.set(themeResPrefix) + outputLocation.set(OutputLocation.SRC) dimensions { multiplier(2f) breakPoints { @@ -50,6 +44,13 @@ themeBuilder { medium(560) } } + theme { + source(url = themeUrl, name = themeAlias) + mode.set(ThemeBuilderMode.THEME) + } + components { + source(name = componentsName, version = componentsVersion, alias = themeAlias) + } } dependencies { diff --git a/tokens/plasma-stards-view/docs/build.gradle.kts b/tokens/plasma-stards-view/docs/build.gradle.kts index 972b99117d..3ced203d03 100644 --- a/tokens/plasma-stards-view/docs/build.gradle.kts +++ b/tokens/plasma-stards-view/docs/build.gradle.kts @@ -1,6 +1,12 @@ +import extensions.docs.DocusaurusExtension + @Suppress("DSL_SCOPE_VIOLATION") plugins { - id("convention.documentation-view") + id("convention.android-lib") + id("convention.integration-detekt") + id("convention.docusaurus") + id("com.google.devtools.ksp") + id(libs.plugins.dsbuilder.get().pluginId) id("convention.testing") alias(libs.plugins.roborazzi) } @@ -9,7 +15,31 @@ android { namespace = "com.sdkit.star.designsystem.docs" } +ksp { + arg("packageName", "com.sdkit.star.designsystem.docs") +} + +dsBuilder { + autoGenerate.set(false) + documentation { + view() + } +} + +extensions.configure("docusaurus") { + components.set(layout.projectDirectory.file("../.sdds/config-info-view-system.json")) + snippetsDir.set(layout.projectDirectory.dir("../.sdds/temp/docs/assets/examples")) +} + +tasks.named("docusaurusGenerate") { + dependsOn("documentationAggregate") +} + dependencies { + "sddsCoreDocumentation"("integration-core:uikit-fixtures:unspecified:docs@jar") + implementation("sdds-core:docs") + ksp("sdds-core:docs") + testImplementation("integration-core:uikit-fixtures") implementation(project(":plasma-stards-view")) implementation(libs.sdds.uikit) implementation(libs.base.androidX.core) diff --git a/tokens/plasma-stards-view/integration/build.gradle.kts b/tokens/plasma-stards-view/integration/build.gradle.kts index 112c65d4d5..8b4e677fcb 100644 --- a/tokens/plasma-stards-view/integration/build.gradle.kts +++ b/tokens/plasma-stards-view/integration/build.gradle.kts @@ -1,13 +1,27 @@ +import com.sdds.plugin.themebuilder.OutputLocation + @Suppress("DSL_SCOPE_VIOLATION") plugins { id("convention.android-lib") - id("convention.integration-view") + id("convention.integration-detekt") + id(libs.plugins.dsbuilder.get().pluginId) } android { namespace = "com.sdkit.star.designsystem.integration" } +dsBuilder { + outputLocation.set(OutputLocation.SRC) + autoGenerate.set(false) + sandbox { + view { + generatedPackageName.set("com.sdkit.star.designsystem.integration") + themeAlias.set("StarDs") + } + } +} + dependencies { implementation("integration-core:sandbox-core") implementation("integration-core:sandbox-compose") diff --git a/tokens/plasma-stards-view/integration/gradle.properties b/tokens/plasma-stards-view/integration/gradle.properties deleted file mode 100644 index 880d6459b7..0000000000 --- a/tokens/plasma-stards-view/integration/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -integration.view.config-path=../config-info-view-system.json -integration.view.package-name=com.sdkit.star.designsystem.integration \ No newline at end of file diff --git a/tokens/plasma.giga.compose/build.gradle.kts b/tokens/plasma.giga.compose/build.gradle.kts index c427b617b3..ec48a13235 100644 --- a/tokens/plasma.giga.compose/build.gradle.kts +++ b/tokens/plasma.giga.compose/build.gradle.kts @@ -14,7 +14,7 @@ plugins { id("convention.maven-publish") id("convention.auto-bump") id("convention.testing-compose") - id(libs.plugins.themebuilder.get().pluginId) + id(libs.plugins.dsbuilder.get().pluginId) alias(libs.plugins.roborazzi) id("convention.docusaurus") } @@ -24,16 +24,19 @@ android { resourcePrefix = themeResPrefix } -themeBuilder { - themeSource(name = themeName, version = themeVersion, alias = themeAlias) - componentSource(name = componentsName, version = componentsVersion, alias = themeAlias) - compose { - componentsMetaStyleClass(true) +dsBuilder { + autoGenerate.set(false) + compose() + packageName.set("com.sdds.plasma.giga") + outputLocation.set(SRC) + theme { + source(name = themeName, version = themeVersion, alias = themeAlias) + mode.set(THEME) + } + components { + source(name = componentsName, version = componentsVersion, alias = themeAlias) + componentsMetaStyleClass.set(true) } - ktPackage(ktPackage = "com.sdds.plasma.giga") - mode(THEME) - outputLocation(SRC) - autoGenerate(false) } dependencies { diff --git a/tokens/plasma.giga.compose/docs/build.gradle.kts b/tokens/plasma.giga.compose/docs/build.gradle.kts index 184177495e..dd4f1fcd24 100644 --- a/tokens/plasma.giga.compose/docs/build.gradle.kts +++ b/tokens/plasma.giga.compose/docs/build.gradle.kts @@ -1,6 +1,13 @@ +import extensions.docs.DocusaurusExtension + @Suppress("DSL_SCOPE_VIOLATION") plugins { - id("convention.documentation-compose") + id("convention.android-lib") + id("convention.integration-detekt") + id("convention.docusaurus") + id("convention.compose") + id("com.google.devtools.ksp") + id(libs.plugins.dsbuilder.get().pluginId) id("convention.testing-compose") alias(libs.plugins.roborazzi) } @@ -9,8 +16,32 @@ android { namespace = "com.sdds.plasma.giga.compose.docs" } +ksp { + arg("packageName", "com.sdds.plasma.giga.compose.docs") +} + +dsBuilder { + autoGenerate.set(false) + documentation { + compose() + } +} + +extensions.configure("docusaurus") { + components.set(layout.projectDirectory.file("../.sdds/config-info-compose.json")) + snippetsDir.set(layout.projectDirectory.dir("../.sdds/temp/docs/assets/examples")) +} + +tasks.named("docusaurusGenerate") { + dependsOn("documentationAggregate") +} + dependencies { + "sddsCoreDocumentation"("integration-core:uikit-compose-fixtures:unspecified:docs@jar") + implementation("sdds-core:docs") + ksp("sdds-core:docs") implementation(project(":plasma.giga.compose")) + testImplementation("integration-core:uikit-compose-fixtures") implementation(libs.sdds.uikit.compose) implementation(libs.base.androidX.compose.foundation) } diff --git a/tokens/plasma.giga.compose/integration/build.gradle.kts b/tokens/plasma.giga.compose/integration/build.gradle.kts index c689905694..748c5ce21f 100644 --- a/tokens/plasma.giga.compose/integration/build.gradle.kts +++ b/tokens/plasma.giga.compose/integration/build.gradle.kts @@ -1,8 +1,22 @@ +import com.sdds.plugin.themebuilder.OutputLocation + @Suppress("DSL_SCOPE_VIOLATION") plugins { id("convention.android-lib") - id("convention.integration-compose") + id("convention.integration-detekt") id("convention.compose") + id(libs.plugins.dsbuilder.get().pluginId) +} + +dsBuilder { + outputLocation.set(OutputLocation.SRC) + autoGenerate.set(false) + sandbox { + compose { + generatedPackageName.set("com.sdds.plasma.giga.integration") + themeAlias.set("PlasmaGiga") + } + } } android { diff --git a/tokens/plasma.giga.compose/integration/gradle.properties b/tokens/plasma.giga.compose/integration/gradle.properties deleted file mode 100644 index 80dd0b1c71..0000000000 --- a/tokens/plasma.giga.compose/integration/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -integration.compose.config-path=../tokens/plasma.giga.compose/.sdds/config-info-compose.json -integration.compose.package-name=com.sdds.plasma.giga.integration -integration.compose.scheme=V2 \ No newline at end of file diff --git a/tokens/plasma.homeds.compose/build.gradle.kts b/tokens/plasma.homeds.compose/build.gradle.kts index 1905bcaa49..e4e875967e 100644 --- a/tokens/plasma.homeds.compose/build.gradle.kts +++ b/tokens/plasma.homeds.compose/build.gradle.kts @@ -15,7 +15,7 @@ plugins { id("convention.maven-publish") id("convention.auto-bump") id("convention.testing-compose") - id(libs.plugins.themebuilder.get().pluginId) + id(libs.plugins.dsbuilder.get().pluginId) alias(libs.plugins.roborazzi) } @@ -24,16 +24,19 @@ android { resourcePrefix = themeResPrefix } -themeBuilder { - componentSource(name = componentsName, version = componentsVersion, alias = themeAlias) - compose { - componentsMetaStyleClass(true) +dsBuilder { + autoGenerate.set(false) + compose() + packageName.set("com.sdds.plasma.homeds") + outputLocation.set(SRC) + theme { + mode.set(THEME) + defaultTypography.set(SMALL) + } + components { + source(name = componentsName, version = componentsVersion, alias = themeAlias) + componentsMetaStyleClass.set(true) } - ktPackage(ktPackage = "com.sdds.plasma.homeds") - mode(THEME) - outputLocation(SRC) - autoGenerate(false) - defaultTypography(SMALL) } dependencies { diff --git a/tokens/plasma.homeds.compose/docs/build.gradle.kts b/tokens/plasma.homeds.compose/docs/build.gradle.kts index 300bafc3c9..14ff8e09a5 100644 --- a/tokens/plasma.homeds.compose/docs/build.gradle.kts +++ b/tokens/plasma.homeds.compose/docs/build.gradle.kts @@ -1,6 +1,13 @@ +import extensions.docs.DocusaurusExtension + @Suppress("DSL_SCOPE_VIOLATION") plugins { - id("convention.documentation-compose") + id("convention.android-lib") + id("convention.integration-detekt") + id("convention.docusaurus") + id("convention.compose") + id("com.google.devtools.ksp") + id(libs.plugins.dsbuilder.get().pluginId) id("convention.testing-compose") alias(libs.plugins.roborazzi) } @@ -13,7 +20,31 @@ docusaurus { additionalComponentNames.add("NumberPanel") } +ksp { + arg("packageName", "com.sdds.plasma.homeds.compose.docs") +} + +dsBuilder { + autoGenerate.set(false) + documentation { + compose() + } +} + +extensions.configure("docusaurus") { + components.set(layout.projectDirectory.file("../.sdds/config-info-compose.json")) + snippetsDir.set(layout.projectDirectory.dir("../.sdds/temp/docs/assets/examples")) +} + +tasks.named("docusaurusGenerate") { + dependsOn("documentationAggregate") +} + dependencies { + "sddsCoreDocumentation"("integration-core:uikit-compose-fixtures:unspecified:docs@jar") + implementation("sdds-core:docs") + ksp("sdds-core:docs") + testImplementation("integration-core:uikit-compose-fixtures") implementation(project(":plasma.homeds.compose")) implementation(libs.sdds.uikit.compose) implementation(icons.sdds.icons) diff --git a/tokens/plasma.homeds.compose/docs/override-docs/docs/components/FloatingButtonBar.md b/tokens/plasma.homeds.compose/docs/override-docs/docs/components/FloatingButtonBar.md index 8e327612c0..b644489813 100644 --- a/tokens/plasma.homeds.compose/docs/override-docs/docs/components/FloatingButtonBar.md +++ b/tokens/plasma.homeds.compose/docs/override-docs/docs/components/FloatingButtonBar.md @@ -1,7 +1,7 @@ --- title: FloatingButtonBar --- -Группа кнопок [ButtonGroup](../../../../../../build-system/docs-template/compose-template/docs/components/ButtonGroupUsage.md), расположенная в компоненте [Overlay](../../../../../../build-system/docs-template/compose-template/docs/components/OverlayUsage.md). +Группа кнопок [ButtonGroup](ButtonGroupUsage.md), расположенная в компоненте [Overlay](OverlayUsage.md). ### Пример создания FloatingButtonBar на основе группы кнопок BasicButton @@ -9,5 +9,5 @@ title: FloatingButtonBar // @sample: com/sdds/plasma/homeds/docs/samples/FloatingButtonBar_WithOverlay.kt ``` -Поскольку FloatingButtonBar основан на компоненте [Overlay](../../../../../../build-system/docs-template/compose-template/docs/components/OverlayUsage.md), для выравнивания контента -внутри overlay, используйте стандартные Modifier для Box. \ No newline at end of file +Поскольку FloatingButtonBar основан на компоненте [Overlay](OverlayUsage.md), для выравнивания контента +внутри overlay, используйте стандартные Modifier для Box. diff --git a/tokens/plasma.homeds.compose/docs/override-docs/structure.json b/tokens/plasma.homeds.compose/docs/override-docs/structure.json new file mode 100644 index 0000000000..4b9bffbbc8 --- /dev/null +++ b/tokens/plasma.homeds.compose/docs/override-docs/structure.json @@ -0,0 +1,15 @@ +{ + "schemaVersion": "1.0", + "navigation": [ + { + "title": "Компоненты", + "items": [ + {"title": "CollapsingNavigationBar", "path": "components/CollapsingNavigationBarUsage.md", "merge": "append"}, + {"title": "Editable", "path": "components/EditableUsage.md", "merge": "append"}, + {"title": "NavigationBar", "path": "components/NavigationBarUsage.md", "merge": "append"}, + {"title": "FloatingButtonBar", "path": "components/FloatingButtonBar.md"}, + {"title": "NumberPanel", "path": "components/NumberPanelUsage.md"} + ] + } + ] +} diff --git a/tokens/plasma.homeds.compose/integration/build.gradle.kts b/tokens/plasma.homeds.compose/integration/build.gradle.kts index 88bad7dfa4..cebc441fb6 100644 --- a/tokens/plasma.homeds.compose/integration/build.gradle.kts +++ b/tokens/plasma.homeds.compose/integration/build.gradle.kts @@ -1,7 +1,10 @@ +import com.sdds.plugin.themebuilder.OutputLocation + @Suppress("DSL_SCOPE_VIOLATION") plugins { id("convention.android-lib") - id("convention.integration-compose") + id("convention.integration-detekt") + id(libs.plugins.dsbuilder.get().pluginId) id("convention.compose") } @@ -9,6 +12,17 @@ android { namespace = "com.sdds.plasma.homeds.compose.integration" } +dsBuilder { + outputLocation.set(OutputLocation.SRC) + autoGenerate.set(false) + sandbox { + compose { + generatedPackageName.set("com.sdds.plasma.homeds.integration") + themeAlias.set("PlasmaHomeDs") + } + } +} + dependencies { implementation(project(":plasma.homeds.compose")) implementation("integration-core:sandbox-core") diff --git a/tokens/plasma.homeds.compose/integration/gradle.properties b/tokens/plasma.homeds.compose/integration/gradle.properties deleted file mode 100644 index 408471469f..0000000000 --- a/tokens/plasma.homeds.compose/integration/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -integration.compose.config-path=../tokens/plasma.homeds.compose/.sdds/config-info-compose.json -integration.compose.package-name=com.sdds.plasma.homeds.integration -integration.compose.scheme=V2 \ No newline at end of file diff --git a/tokens/plasma.sd.service.compose/build.gradle.kts b/tokens/plasma.sd.service.compose/build.gradle.kts index bf4b3103f6..6a08aec228 100644 --- a/tokens/plasma.sd.service.compose/build.gradle.kts +++ b/tokens/plasma.sd.service.compose/build.gradle.kts @@ -14,7 +14,7 @@ plugins { id("convention.maven-publish") id("convention.auto-bump") id("convention.testing-compose") - id(libs.plugins.themebuilder.get().pluginId) + id(libs.plugins.dsbuilder.get().pluginId) alias(libs.plugins.roborazzi) id("convention.docusaurus") } @@ -24,16 +24,19 @@ android { resourcePrefix = themeResPrefix } -themeBuilder { - themeSource(name = themeName, version = themeVersion, alias = themeAlias) - componentSource(name = componentsName, version = componentsVersion, alias = themeAlias) - compose { - componentsMetaStyleClass(true) +dsBuilder { + autoGenerate.set(false) + compose() + packageName.set("com.sdds.plasma.sd.service") + outputLocation.set(SRC) + theme { + source(name = themeName, version = themeVersion, alias = themeAlias) + mode.set(THEME) + } + components { + source(name = componentsName, version = componentsVersion, alias = themeAlias) + componentsMetaStyleClass.set(true) } - ktPackage(ktPackage = "com.sdds.plasma.sd.service") - mode(THEME) - outputLocation(SRC) - autoGenerate(false) } dependencies { diff --git a/tokens/plasma.sd.service.compose/docs/build.gradle.kts b/tokens/plasma.sd.service.compose/docs/build.gradle.kts index ebb971ffc5..4a210983ea 100644 --- a/tokens/plasma.sd.service.compose/docs/build.gradle.kts +++ b/tokens/plasma.sd.service.compose/docs/build.gradle.kts @@ -1,6 +1,13 @@ +import extensions.docs.DocusaurusExtension + @Suppress("DSL_SCOPE_VIOLATION") plugins { - id("convention.documentation-compose") + id("convention.android-lib") + id("convention.integration-detekt") + id("convention.docusaurus") + id("convention.compose") + id("com.google.devtools.ksp") + id(libs.plugins.dsbuilder.get().pluginId) id("convention.testing-compose") alias(libs.plugins.roborazzi) } @@ -9,7 +16,31 @@ android { namespace = "com.sdds.plasma.sd.service.compose.docs" } +ksp { + arg("packageName", "com.sdds.plasma.sd.service.compose.docs") +} + +dsBuilder { + autoGenerate.set(false) + documentation { + compose() + } +} + +extensions.configure("docusaurus") { + components.set(layout.projectDirectory.file("../.sdds/config-info-compose.json")) + snippetsDir.set(layout.projectDirectory.dir("../.sdds/temp/docs/assets/examples")) +} + +tasks.named("docusaurusGenerate") { + dependsOn("documentationAggregate") +} + dependencies { + "sddsCoreDocumentation"("integration-core:uikit-compose-fixtures:unspecified:docs@jar") + implementation("sdds-core:docs") + ksp("sdds-core:docs") + testImplementation("integration-core:uikit-compose-fixtures") implementation(project(":plasma.sd.service.compose")) implementation(libs.sdds.uikit.compose) implementation(libs.base.androidX.compose.foundation) diff --git a/tokens/plasma.sd.service.compose/integration/build.gradle.kts b/tokens/plasma.sd.service.compose/integration/build.gradle.kts index 1765a9770a..54e2f8e1e1 100644 --- a/tokens/plasma.sd.service.compose/integration/build.gradle.kts +++ b/tokens/plasma.sd.service.compose/integration/build.gradle.kts @@ -1,7 +1,10 @@ +import com.sdds.plugin.themebuilder.OutputLocation + @Suppress("DSL_SCOPE_VIOLATION") plugins { id("convention.android-lib") - id("convention.integration-compose") + id("convention.integration-detekt") + id(libs.plugins.dsbuilder.get().pluginId) id("convention.compose") } @@ -9,6 +12,17 @@ android { namespace = "com.sdds.plasma.sd.service.compose.integration" } +dsBuilder { + outputLocation.set(OutputLocation.SRC) + autoGenerate.set(false) + sandbox { + compose { + generatedPackageName.set("com.sdds.plasma.sd.service.compose.integration") + themeAlias.set("PlasmaSdService") + } + } +} + dependencies { implementation(project(":plasma.sd.service.compose")) implementation("integration-core:sandbox-core") diff --git a/tokens/plasma.sd.service.compose/integration/gradle.properties b/tokens/plasma.sd.service.compose/integration/gradle.properties deleted file mode 100644 index a976ecc2de..0000000000 --- a/tokens/plasma.sd.service.compose/integration/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -integration.compose.config-path=../tokens/plasma.sd.service.compose/.sdds/config-info-compose.json -integration.compose.package-name=com.sdds.plasma.sd.service.compose.integration -integration.compose.scheme=V2 \ No newline at end of file diff --git a/tokens/plasma.sd.service.view/config-info-view-system.json b/tokens/plasma.sd.service.view/.sdds/config-info-view-system.json similarity index 98% rename from tokens/plasma.sd.service.view/config-info-view-system.json rename to tokens/plasma.sd.service.view/.sdds/config-info-view-system.json index ca8a8f4a4b..974c90dbc7 100644 --- a/tokens/plasma.sd.service.view/config-info-view-system.json +++ b/tokens/plasma.sd.service.view/.sdds/config-info-view-system.json @@ -29,6 +29,28 @@ } ] }, + { + "key": "indicator", + "coreName": "Indicator", + "styleName": "Indicator", + "variations": [ + { + "name": "l", + "viewReference": "Plasma.SdService.Components.Indicator.L", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.IndicatorL" + }, + { + "name": "m", + "viewReference": "Plasma.SdService.Components.Indicator.M", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.IndicatorM" + }, + { + "name": "s", + "viewReference": "Plasma.SdService.Components.Indicator.S", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.IndicatorS" + } + ] + }, { "key": "avatar-group", "coreName": "AvatarGroup", @@ -3661,6 +3683,41 @@ "coreName": "Chip", "styleName": "EmbeddedChip", "variations": [ + { + "name": "xl", + "viewReference": "Plasma.SdService.Components.EmbeddedChip.Xl", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.EmbeddedChipXl" + }, + { + "name": "xl.default", + "viewReference": "Plasma.SdService.Components.EmbeddedChip.Xl.Default", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.EmbeddedChipXlDefault" + }, + { + "name": "xl.accent", + "viewReference": "Plasma.SdService.Components.EmbeddedChip.Xl.Accent", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.EmbeddedChipXlAccent" + }, + { + "name": "xl.negative", + "viewReference": "Plasma.SdService.Components.EmbeddedChip.Xl.Negative", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.EmbeddedChipXlNegative" + }, + { + "name": "xl.positive", + "viewReference": "Plasma.SdService.Components.EmbeddedChip.Xl.Positive", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.EmbeddedChipXlPositive" + }, + { + "name": "xl.secondary", + "viewReference": "Plasma.SdService.Components.EmbeddedChip.Xl.Secondary", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.EmbeddedChipXlSecondary" + }, + { + "name": "xl.warning", + "viewReference": "Plasma.SdService.Components.EmbeddedChip.Xl.Warning", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.EmbeddedChipXlWarning" + }, { "name": "l", "viewReference": "Plasma.SdService.Components.EmbeddedChip.L", @@ -4382,6 +4439,41 @@ "coreName": "ChipGroup", "styleName": "EmbeddedChipGroupDense", "variations": [ + { + "name": "xl", + "viewReference": "Plasma.SdService.Components.EmbeddedChipGroupDense.Xl", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.EmbeddedChipGroupDenseXl" + }, + { + "name": "xl.default", + "viewReference": "Plasma.SdService.Components.EmbeddedChipGroupDense.Xl.Default", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.EmbeddedChipGroupDenseXlDefault" + }, + { + "name": "xl.accent", + "viewReference": "Plasma.SdService.Components.EmbeddedChipGroupDense.Xl.Accent", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.EmbeddedChipGroupDenseXlAccent" + }, + { + "name": "xl.negative", + "viewReference": "Plasma.SdService.Components.EmbeddedChipGroupDense.Xl.Negative", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.EmbeddedChipGroupDenseXlNegative" + }, + { + "name": "xl.positive", + "viewReference": "Plasma.SdService.Components.EmbeddedChipGroupDense.Xl.Positive", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.EmbeddedChipGroupDenseXlPositive" + }, + { + "name": "xl.secondary", + "viewReference": "Plasma.SdService.Components.EmbeddedChipGroupDense.Xl.Secondary", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.EmbeddedChipGroupDenseXlSecondary" + }, + { + "name": "xl.warning", + "viewReference": "Plasma.SdService.Components.EmbeddedChipGroupDense.Xl.Warning", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.EmbeddedChipGroupDenseXlWarning" + }, { "name": "l", "viewReference": "Plasma.SdService.Components.EmbeddedChipGroupDense.L", @@ -4529,6 +4621,41 @@ "coreName": "ChipGroup", "styleName": "EmbeddedChipGroupWide", "variations": [ + { + "name": "xl", + "viewReference": "Plasma.SdService.Components.EmbeddedChipGroupWide.Xl", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.EmbeddedChipGroupWideXl" + }, + { + "name": "xl.default", + "viewReference": "Plasma.SdService.Components.EmbeddedChipGroupWide.Xl.Default", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.EmbeddedChipGroupWideXlDefault" + }, + { + "name": "xl.accent", + "viewReference": "Plasma.SdService.Components.EmbeddedChipGroupWide.Xl.Accent", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.EmbeddedChipGroupWideXlAccent" + }, + { + "name": "xl.negative", + "viewReference": "Plasma.SdService.Components.EmbeddedChipGroupWide.Xl.Negative", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.EmbeddedChipGroupWideXlNegative" + }, + { + "name": "xl.positive", + "viewReference": "Plasma.SdService.Components.EmbeddedChipGroupWide.Xl.Positive", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.EmbeddedChipGroupWideXlPositive" + }, + { + "name": "xl.secondary", + "viewReference": "Plasma.SdService.Components.EmbeddedChipGroupWide.Xl.Secondary", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.EmbeddedChipGroupWideXlSecondary" + }, + { + "name": "xl.warning", + "viewReference": "Plasma.SdService.Components.EmbeddedChipGroupWide.Xl.Warning", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.EmbeddedChipGroupWideXlWarning" + }, { "name": "l", "viewReference": "Plasma.SdService.Components.EmbeddedChipGroupWide.L", @@ -4878,6 +5005,88 @@ } ] }, + { + "key": "counter", + "coreName": "Counter", + "styleName": "Counter", + "variations": [ + { + "name": "l", + "viewReference": "Plasma.SdService.Components.Counter.L", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.CounterL" + }, + { + "name": "l.default", + "viewReference": "Plasma.SdService.Components.Counter.L.Default", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.CounterLDefault" + }, + { + "name": "l.accent", + "viewReference": "Plasma.SdService.Components.Counter.L.Accent", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.CounterLAccent" + }, + { + "name": "m", + "viewReference": "Plasma.SdService.Components.Counter.M", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.CounterM" + }, + { + "name": "m.default", + "viewReference": "Plasma.SdService.Components.Counter.M.Default", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.CounterMDefault" + }, + { + "name": "m.accent", + "viewReference": "Plasma.SdService.Components.Counter.M.Accent", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.CounterMAccent" + }, + { + "name": "s", + "viewReference": "Plasma.SdService.Components.Counter.S", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.CounterS" + }, + { + "name": "s.default", + "viewReference": "Plasma.SdService.Components.Counter.S.Default", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.CounterSDefault" + }, + { + "name": "s.accent", + "viewReference": "Plasma.SdService.Components.Counter.S.Accent", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.CounterSAccent" + }, + { + "name": "xs", + "viewReference": "Plasma.SdService.Components.Counter.Xs", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.CounterXs" + }, + { + "name": "xs.default", + "viewReference": "Plasma.SdService.Components.Counter.Xs.Default", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.CounterXsDefault" + }, + { + "name": "xs.accent", + "viewReference": "Plasma.SdService.Components.Counter.Xs.Accent", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.CounterXsAccent" + }, + { + "name": "xxs", + "viewReference": "Plasma.SdService.Components.Counter.Xxs", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.CounterXxs" + }, + { + "name": "xxs.default", + "viewReference": "Plasma.SdService.Components.Counter.Xxs.Default", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.CounterXxsDefault" + }, + { + "name": "xxs.accent", + "viewReference": "Plasma.SdService.Components.Counter.Xxs.Accent", + "viewOverlayReference": "Plasma.SdService.ComponentOverlays.CounterXxsAccent" + } + ] + }, { "key": "divider", "coreName": "Divider", diff --git a/tokens/plasma.sd.service.view/.sdds/theme-info-view-system.json b/tokens/plasma.sd.service.view/.sdds/theme-info-view-system.json new file mode 100644 index 0000000000..cb3b136eb0 --- /dev/null +++ b/tokens/plasma.sd.service.view/.sdds/theme-info-view-system.json @@ -0,0 +1,6 @@ +{ + "name": "SdService", + "version": "0.1.0", + "platform": "vs", + "tokens": [] +} \ No newline at end of file diff --git a/tokens/plasma.sd.service.view/build.gradle.kts b/tokens/plasma.sd.service.view/build.gradle.kts index 7bd5db5dc9..969e1a2567 100644 --- a/tokens/plasma.sd.service.view/build.gradle.kts +++ b/tokens/plasma.sd.service.view/build.gradle.kts @@ -11,7 +11,7 @@ import utils.themeVersion @Suppress("DSL_SCOPE_VIOLATION") plugins { id("convention.android-lib") - id(libs.plugins.themebuilder.get().pluginId) + id(libs.plugins.dsbuilder.get().pluginId) id("convention.maven-publish") id("convention.auto-bump") id("convention.testing") @@ -24,19 +24,23 @@ android { resourcePrefix = themeResPrefix } -themeBuilder { - themeSource(name = themeName, version = themeVersion, alias = themeAlias) - componentSource(name = componentsName, version = componentsVersion, alias = themeAlias) +dsBuilder { + autoGenerate.set(false) view { themeParents { materialComponentsTheme() } setupShapeAppearance(sddsShape()) } - ktPackage("com.sdds.plasma.sd.service") - autoGenerate(false) - mode(ThemeBuilderMode.THEME) - outputLocation(OutputLocation.SRC) + packageName.set("com.sdds.plasma.sd.service") + outputLocation.set(OutputLocation.SRC) + theme { + source(name = themeName, version = themeVersion, alias = themeAlias) + mode.set(ThemeBuilderMode.THEME) + } + components { + source(name = componentsName, version = componentsVersion, alias = themeAlias) + } } dependencies { diff --git a/tokens/plasma.sd.service.view/docs/build.gradle.kts b/tokens/plasma.sd.service.view/docs/build.gradle.kts index 4be39e7e0e..e3a883fe51 100644 --- a/tokens/plasma.sd.service.view/docs/build.gradle.kts +++ b/tokens/plasma.sd.service.view/docs/build.gradle.kts @@ -1,6 +1,12 @@ +import extensions.docs.DocusaurusExtension + @Suppress("DSL_SCOPE_VIOLATION") plugins { - id("convention.documentation-view") + id("convention.android-lib") + id("convention.integration-detekt") + id("convention.docusaurus") + id("com.google.devtools.ksp") + id(libs.plugins.dsbuilder.get().pluginId) id("convention.testing") alias(libs.plugins.roborazzi) } @@ -9,7 +15,31 @@ android { namespace = "com.sdds.plasma.sd.service.docs" } +ksp { + arg("packageName", "com.sdds.plasma.sd.service.docs") +} + +dsBuilder { + autoGenerate.set(false) + documentation { + view() + } +} + +extensions.configure("docusaurus") { + components.set(layout.projectDirectory.file("../.sdds/config-info-view-system.json")) + snippetsDir.set(layout.projectDirectory.dir("../.sdds/temp/docs/assets/examples")) +} + +tasks.named("docusaurusGenerate") { + dependsOn("documentationAggregate") +} + dependencies { + "sddsCoreDocumentation"("integration-core:uikit-fixtures:unspecified:docs@jar") + implementation("sdds-core:docs") + ksp("sdds-core:docs") + testImplementation("integration-core:uikit-fixtures") implementation(project(":plasma.sd.service.view")) implementation(libs.sdds.uikit) implementation(libs.base.androidX.core) diff --git a/tokens/plasma.sd.service.view/integration/build.gradle.kts b/tokens/plasma.sd.service.view/integration/build.gradle.kts index 60f48ec413..808f2e7898 100644 --- a/tokens/plasma.sd.service.view/integration/build.gradle.kts +++ b/tokens/plasma.sd.service.view/integration/build.gradle.kts @@ -1,13 +1,27 @@ +import com.sdds.plugin.themebuilder.OutputLocation + @Suppress("DSL_SCOPE_VIOLATION") plugins { id("convention.android-lib") - id("convention.integration-view") + id("convention.integration-detekt") + id(libs.plugins.dsbuilder.get().pluginId) } android { namespace = "com.sdds.sd.service.sandbox.integration" } +dsBuilder { + outputLocation.set(OutputLocation.SRC) + autoGenerate.set(false) + sandbox { + view { + generatedPackageName.set("com.plasma.sd.service.integration") + themeAlias.set("SdService") + } + } +} + dependencies { implementation("integration-core:sandbox-core") implementation("integration-core:sandbox-compose") diff --git a/tokens/plasma.sd.service.view/integration/gradle.properties b/tokens/plasma.sd.service.view/integration/gradle.properties deleted file mode 100644 index 32b5151cd2..0000000000 --- a/tokens/plasma.sd.service.view/integration/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -integration.view.config-path=../config-info-view-system.json -integration.view.package-name=com.plasma.sd.service.integration \ No newline at end of file diff --git a/tokens/sdds-sbcom-compose/build.gradle.kts b/tokens/sdds-sbcom-compose/build.gradle.kts index a65d146c64..584dc3142a 100644 --- a/tokens/sdds-sbcom-compose/build.gradle.kts +++ b/tokens/sdds-sbcom-compose/build.gradle.kts @@ -14,7 +14,7 @@ plugins { id("convention.maven-publish") id("convention.auto-bump") id("convention.testing-compose") - id(libs.plugins.themebuilder.get().pluginId) + id(libs.plugins.dsbuilder.get().pluginId) alias(libs.plugins.roborazzi) } @@ -23,17 +23,20 @@ android { resourcePrefix = themeResPrefix } -themeBuilder { - componentSource(name = componentsName, version = componentsVersion, alias = themeAlias) - compose { - componentsMetaStyleClass(true) +dsBuilder { + autoGenerate.set(false) + compose() + packageName.set("com.sdds.sbcom") + outputLocation.set(SRC) + theme { + mode.set(THEME) + ignoreDisabledTokens.set(true) + useDefaultFonts.set(true) + } + components { + source(name = componentsName, version = componentsVersion, alias = themeAlias) + componentsMetaStyleClass.set(true) } - ktPackage(ktPackage = "com.sdds.sbcom") - mode(THEME) - outputLocation(SRC) - autoGenerate(false) - ignoreDisabledTokens(true) - useDefaultFonts(true) } dependencies { diff --git a/tokens/sdds-sbcom-compose/docs/build.gradle.kts b/tokens/sdds-sbcom-compose/docs/build.gradle.kts index 127330e74d..4346a12cfd 100644 --- a/tokens/sdds-sbcom-compose/docs/build.gradle.kts +++ b/tokens/sdds-sbcom-compose/docs/build.gradle.kts @@ -1,6 +1,13 @@ +import extensions.docs.DocusaurusExtension + @Suppress("DSL_SCOPE_VIOLATION") plugins { - id("convention.documentation-compose") + id("convention.android-lib") + id("convention.integration-detekt") + id("convention.docusaurus") + id("convention.compose") + id("com.google.devtools.ksp") + id(libs.plugins.dsbuilder.get().pluginId) id("convention.testing-compose") alias(libs.plugins.roborazzi) } @@ -9,7 +16,31 @@ android { namespace = "com.sdds.sbcom.compose.docs" } +ksp { + arg("packageName", "com.sdds.sbcom.compose.docs") +} + +dsBuilder { + autoGenerate.set(false) + documentation { + compose() + } +} + +extensions.configure("docusaurus") { + components.set(layout.projectDirectory.file("../.sdds/config-info-compose.json")) + snippetsDir.set(layout.projectDirectory.dir("../.sdds/temp/docs/assets/examples")) +} + +tasks.named("docusaurusGenerate") { + dependsOn("documentationAggregate") +} + dependencies { + "sddsCoreDocumentation"("integration-core:uikit-compose-fixtures:unspecified:docs@jar") + implementation("sdds-core:docs") + ksp("sdds-core:docs") + testImplementation("integration-core:uikit-compose-fixtures") implementation(project(":sdds-sbcom-compose")) implementation(libs.sdds.uikit.compose) implementation(icons.sdds.icons) diff --git a/tokens/sdds-sbcom-compose/docs/override-docs/structure.json b/tokens/sdds-sbcom-compose/docs/override-docs/structure.json new file mode 100644 index 0000000000..24bf46cd7e --- /dev/null +++ b/tokens/sdds-sbcom-compose/docs/override-docs/structure.json @@ -0,0 +1,11 @@ +{ + "schemaVersion": "1.0", + "navigation": [ + { + "title": "Визуальные эффекты", + "items": [ + {"title": "Indication", "path": "graphics/IndicationUsage.md", "merge": "append"} + ] + } + ] +} diff --git a/tokens/sdds-sbcom-compose/integration/build.gradle.kts b/tokens/sdds-sbcom-compose/integration/build.gradle.kts index 01ea4ffd76..5fe42935f0 100644 --- a/tokens/sdds-sbcom-compose/integration/build.gradle.kts +++ b/tokens/sdds-sbcom-compose/integration/build.gradle.kts @@ -1,7 +1,10 @@ +import com.sdds.plugin.themebuilder.OutputLocation + @Suppress("DSL_SCOPE_VIOLATION") plugins { id("convention.android-lib") - id("convention.integration-compose") + id("convention.integration-detekt") + id(libs.plugins.dsbuilder.get().pluginId) id("convention.compose") } @@ -9,6 +12,17 @@ android { namespace = "com.sdds.sbcom.compose.integration" } +dsBuilder { + outputLocation.set(OutputLocation.SRC) + autoGenerate.set(false) + sandbox { + compose { + generatedPackageName.set("com.sdds.sbcom.integration") + themeAlias.set("SddsSbCom") + } + } +} + dependencies { implementation(project(":sdds-sbcom-compose")) implementation("integration-core:sandbox-core") diff --git a/tokens/sdds-sbcom-compose/integration/gradle.properties b/tokens/sdds-sbcom-compose/integration/gradle.properties deleted file mode 100644 index 234fd712ee..0000000000 --- a/tokens/sdds-sbcom-compose/integration/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -integration.compose.config-path=../tokens/sdds-sbcom-compose/.sdds/config-info-compose.json -integration.compose.package-name=com.sdds.sbcom.integration -integration.compose.scheme=V2 \ No newline at end of file diff --git a/tokens/sdds.serv.compose/build.gradle.kts b/tokens/sdds.serv.compose/build.gradle.kts index e3c636a9bc..c9f644802d 100644 --- a/tokens/sdds.serv.compose/build.gradle.kts +++ b/tokens/sdds.serv.compose/build.gradle.kts @@ -13,7 +13,7 @@ plugins { id("convention.cmp-lib") id("convention.maven-publish") id("convention.auto-bump") - id(libs.plugins.themebuilder.get().pluginId) + id(libs.plugins.dsbuilder.get().pluginId) alias(libs.plugins.roborazzi) id("convention.docusaurus") } @@ -70,15 +70,17 @@ compose.resources { packageOfResClass = "com.sdds.serv.compose.generated.resources" } -themeBuilder { - themeSource(name = themeName, version = themeVersion, alias = themeAlias) - componentSource(name = componentsName, version = componentsVersion, alias = themeAlias) - compose { - componentsMetaStyleClass(true) - multiplatform(true) +dsBuilder { + autoGenerate.set(false) + compose(multiplatform = true) + packageName.set("com.sdds.serv") + outputLocation.set(SRC) + theme { + source(name = themeName, version = themeVersion, alias = themeAlias) + mode.set(THEME) + } + components { + source(name = componentsName, version = componentsVersion, alias = themeAlias) + componentsMetaStyleClass.set(true) } - ktPackage(ktPackage = "com.sdds.serv") - mode(THEME) - autoGenerate(false) - outputLocation(SRC) } diff --git a/tokens/sdds.serv.compose/docs/build.gradle.kts b/tokens/sdds.serv.compose/docs/build.gradle.kts index 45c516e0a4..c4a447f26d 100644 --- a/tokens/sdds.serv.compose/docs/build.gradle.kts +++ b/tokens/sdds.serv.compose/docs/build.gradle.kts @@ -1,6 +1,13 @@ +import extensions.docs.DocusaurusExtension + @Suppress("DSL_SCOPE_VIOLATION") plugins { - id("convention.documentation-compose") + id("convention.android-lib") + id("convention.integration-detekt") + id("convention.docusaurus") + id("convention.compose") + id("com.google.devtools.ksp") + id(libs.plugins.dsbuilder.get().pluginId) id("convention.testing-compose") alias(libs.plugins.roborazzi) } @@ -9,7 +16,31 @@ android { namespace = "com.sdds.serv.compose.docs" } +ksp { + arg("packageName", "com.sdds.serv.compose.docs") +} + +dsBuilder { + autoGenerate.set(false) + documentation { + compose() + } +} + +extensions.configure("docusaurus") { + components.set(layout.projectDirectory.file("../.sdds/config-info-compose.json")) + snippetsDir.set(layout.projectDirectory.dir("../.sdds/temp/docs/assets/examples")) +} + +tasks.named("docusaurusGenerate") { + dependsOn("documentationAggregate") +} + dependencies { + "sddsCoreDocumentation"("integration-core:uikit-compose-fixtures:unspecified:docs@jar") + implementation("sdds-core:docs") + ksp("sdds-core:docs") + testImplementation("integration-core:uikit-compose-fixtures") implementation(project(":sdds.serv.compose")) implementation(libs.sdds.uikit.compose) implementation(libs.base.androidX.compose.foundation) diff --git a/tokens/sdds.serv.compose/integration/build.gradle.kts b/tokens/sdds.serv.compose/integration/build.gradle.kts index 4e0b594c5c..3ad320f58e 100644 --- a/tokens/sdds.serv.compose/integration/build.gradle.kts +++ b/tokens/sdds.serv.compose/integration/build.gradle.kts @@ -1,9 +1,11 @@ +import com.sdds.plugin.themebuilder.OutputLocation import utils.addDefaultTargets @Suppress("DSL_SCOPE_VIOLATION") plugins { id("convention.cmp-lib") - id("convention.integration-compose") + id("convention.integration-detekt") + id(libs.plugins.dsbuilder.get().pluginId) } android { @@ -15,6 +17,18 @@ kotlin { sourceSets { commonMain { + dsBuilder { + outputLocation.set(OutputLocation.SRC) + autoGenerate.set(false) + sandbox { + compose { + generatedPackageName.set("com.sdds.serv.integration") + themeAlias.set("SddsServ") + multiplatform.set(true) + } + } + } + dependencies { implementation(project(":sdds.serv.compose")) implementation("integration-core:sandbox-core") diff --git a/tokens/sdds.serv.compose/integration/gradle.properties b/tokens/sdds.serv.compose/integration/gradle.properties deleted file mode 100644 index 7ecde9cdff..0000000000 --- a/tokens/sdds.serv.compose/integration/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -integration.compose.config-path=../tokens/sdds.serv.compose/.sdds/config-info-compose.json -integration.compose.package-name=com.sdds.serv.integration -integration.compose.scheme=V2 -integration.compose.multiplatform=true diff --git a/tokens/sdds.serv.view/config-info-view-system.json b/tokens/sdds.serv.view/.sdds/config-info-view-system.json similarity index 95% rename from tokens/sdds.serv.view/config-info-view-system.json rename to tokens/sdds.serv.view/.sdds/config-info-view-system.json index 2c1db82e1b..7fe8086e69 100644 --- a/tokens/sdds.serv.view/config-info-view-system.json +++ b/tokens/sdds.serv.view/.sdds/config-info-view-system.json @@ -8023,6 +8023,100 @@ } ] }, + { + "key": "tool-bar", + "coreName": "ToolBar", + "styleName": "ToolBarHorizontal", + "variations": [ + { + "name": "l", + "viewReference": "Serv.Sdds.Components.ToolBarHorizontal.L", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.ToolBarHorizontalL" + }, + { + "name": "l.has-shadow", + "viewReference": "Serv.Sdds.Components.ToolBarHorizontal.L.HasShadow", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.ToolBarHorizontalLHasShadow" + }, + { + "name": "m", + "viewReference": "Serv.Sdds.Components.ToolBarHorizontal.M", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.ToolBarHorizontalM" + }, + { + "name": "m.has-shadow", + "viewReference": "Serv.Sdds.Components.ToolBarHorizontal.M.HasShadow", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.ToolBarHorizontalMHasShadow" + }, + { + "name": "s", + "viewReference": "Serv.Sdds.Components.ToolBarHorizontal.S", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.ToolBarHorizontalS" + }, + { + "name": "s.has-shadow", + "viewReference": "Serv.Sdds.Components.ToolBarHorizontal.S.HasShadow", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.ToolBarHorizontalSHasShadow" + }, + { + "name": "xs", + "viewReference": "Serv.Sdds.Components.ToolBarHorizontal.Xs", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.ToolBarHorizontalXs" + }, + { + "name": "xs.has-shadow", + "viewReference": "Serv.Sdds.Components.ToolBarHorizontal.Xs.HasShadow", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.ToolBarHorizontalXsHasShadow" + } + ] + }, + { + "key": "tool-bar", + "coreName": "ToolBar", + "styleName": "ToolBarVertical", + "variations": [ + { + "name": "l", + "viewReference": "Serv.Sdds.Components.ToolBarVertical.L", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.ToolBarVerticalL" + }, + { + "name": "l.has-shadow", + "viewReference": "Serv.Sdds.Components.ToolBarVertical.L.HasShadow", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.ToolBarVerticalLHasShadow" + }, + { + "name": "m", + "viewReference": "Serv.Sdds.Components.ToolBarVertical.M", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.ToolBarVerticalM" + }, + { + "name": "m.has-shadow", + "viewReference": "Serv.Sdds.Components.ToolBarVertical.M.HasShadow", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.ToolBarVerticalMHasShadow" + }, + { + "name": "s", + "viewReference": "Serv.Sdds.Components.ToolBarVertical.S", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.ToolBarVerticalS" + }, + { + "name": "s.has-shadow", + "viewReference": "Serv.Sdds.Components.ToolBarVertical.S.HasShadow", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.ToolBarVerticalSHasShadow" + }, + { + "name": "xs", + "viewReference": "Serv.Sdds.Components.ToolBarVertical.Xs", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.ToolBarVerticalXs" + }, + { + "name": "xs.has-shadow", + "viewReference": "Serv.Sdds.Components.ToolBarVertical.Xs.HasShadow", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.ToolBarVerticalXsHasShadow" + } + ] + }, { "key": "toast", "coreName": "Toast", @@ -13282,6 +13376,200 @@ } ] }, + { + "key": "icon-button", + "coreName": "IconButton", + "styleName": "IconButton", + "variations": [ + { + "name": "xl", + "viewReference": "Serv.Sdds.Components.IconButton.Xl", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonXl" + }, + { + "name": "xl.default", + "viewReference": "Serv.Sdds.Components.IconButton.Xl.Default", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonXlDefault" + }, + { + "name": "xl.secondary", + "viewReference": "Serv.Sdds.Components.IconButton.Xl.Secondary", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonXlSecondary" + }, + { + "name": "xl.accent", + "viewReference": "Serv.Sdds.Components.IconButton.Xl.Accent", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonXlAccent" + }, + { + "name": "xl.positive", + "viewReference": "Serv.Sdds.Components.IconButton.Xl.Positive", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonXlPositive" + }, + { + "name": "xl.negative", + "viewReference": "Serv.Sdds.Components.IconButton.Xl.Negative", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonXlNegative" + }, + { + "name": "xl.warning", + "viewReference": "Serv.Sdds.Components.IconButton.Xl.Warning", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonXlWarning" + }, + { + "name": "xl.info", + "viewReference": "Serv.Sdds.Components.IconButton.Xl.Info", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonXlInfo" + }, + { + "name": "l", + "viewReference": "Serv.Sdds.Components.IconButton.L", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonL" + }, + { + "name": "l.default", + "viewReference": "Serv.Sdds.Components.IconButton.L.Default", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonLDefault" + }, + { + "name": "l.secondary", + "viewReference": "Serv.Sdds.Components.IconButton.L.Secondary", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonLSecondary" + }, + { + "name": "l.accent", + "viewReference": "Serv.Sdds.Components.IconButton.L.Accent", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonLAccent" + }, + { + "name": "l.positive", + "viewReference": "Serv.Sdds.Components.IconButton.L.Positive", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonLPositive" + }, + { + "name": "l.negative", + "viewReference": "Serv.Sdds.Components.IconButton.L.Negative", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonLNegative" + }, + { + "name": "l.warning", + "viewReference": "Serv.Sdds.Components.IconButton.L.Warning", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonLWarning" + }, + { + "name": "l.info", + "viewReference": "Serv.Sdds.Components.IconButton.L.Info", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonLInfo" + }, + { + "name": "m", + "viewReference": "Serv.Sdds.Components.IconButton.M", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonM" + }, + { + "name": "m.default", + "viewReference": "Serv.Sdds.Components.IconButton.M.Default", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonMDefault" + }, + { + "name": "m.secondary", + "viewReference": "Serv.Sdds.Components.IconButton.M.Secondary", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonMSecondary" + }, + { + "name": "m.accent", + "viewReference": "Serv.Sdds.Components.IconButton.M.Accent", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonMAccent" + }, + { + "name": "m.positive", + "viewReference": "Serv.Sdds.Components.IconButton.M.Positive", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonMPositive" + }, + { + "name": "m.negative", + "viewReference": "Serv.Sdds.Components.IconButton.M.Negative", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonMNegative" + }, + { + "name": "m.warning", + "viewReference": "Serv.Sdds.Components.IconButton.M.Warning", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonMWarning" + }, + { + "name": "m.info", + "viewReference": "Serv.Sdds.Components.IconButton.M.Info", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonMInfo" + }, + { + "name": "s", + "viewReference": "Serv.Sdds.Components.IconButton.S", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonS" + }, + { + "name": "s.default", + "viewReference": "Serv.Sdds.Components.IconButton.S.Default", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonSDefault" + }, + { + "name": "s.secondary", + "viewReference": "Serv.Sdds.Components.IconButton.S.Secondary", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonSSecondary" + }, + { + "name": "s.accent", + "viewReference": "Serv.Sdds.Components.IconButton.S.Accent", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonSAccent" + }, + { + "name": "s.positive", + "viewReference": "Serv.Sdds.Components.IconButton.S.Positive", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonSPositive" + }, + { + "name": "s.negative", + "viewReference": "Serv.Sdds.Components.IconButton.S.Negative", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonSNegative" + }, + { + "name": "s.warning", + "viewReference": "Serv.Sdds.Components.IconButton.S.Warning", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonSWarning" + }, + { + "name": "s.info", + "viewReference": "Serv.Sdds.Components.IconButton.S.Info", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonSInfo" + } + ] + }, + { + "key": "button-group", + "coreName": "ButtonGroup", + "styleName": "AiHeaderEmbeddedIconButtonGroup", + "variations": [ + { + "name": "s", + "viewReference": "Serv.Sdds.Components.AiHeaderEmbeddedIconButtonGroup.S", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.AiHeaderEmbeddedIconButtonGroupS" + }, + { + "name": "m", + "viewReference": "Serv.Sdds.Components.AiHeaderEmbeddedIconButtonGroup.M", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.AiHeaderEmbeddedIconButtonGroupM" + }, + { + "name": "l", + "viewReference": "Serv.Sdds.Components.AiHeaderEmbeddedIconButtonGroup.L", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.AiHeaderEmbeddedIconButtonGroupL" + }, + { + "name": "xl", + "viewReference": "Serv.Sdds.Components.AiHeaderEmbeddedIconButtonGroup.Xl", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.AiHeaderEmbeddedIconButtonGroupXl" + } + ] + }, { "key": "select-item", "coreName": "SelectIem", @@ -13409,6 +13697,341 @@ "viewOverlayReference": "Serv.Sdds.ComponentOverlays.SelectItemMultipleTightXs" } ] + }, + { + "key": "chip-group", + "coreName": "ChipGroup", + "styleName": "AiAnswerChipGroup", + "variations": [ + { + "name": "l", + "viewReference": "Serv.Sdds.Components.AiAnswerChipGroup.L", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.AiAnswerChipGroupL" + }, + { + "name": "m", + "viewReference": "Serv.Sdds.Components.AiAnswerChipGroup.M", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.AiAnswerChipGroupM" + }, + { + "name": "s", + "viewReference": "Serv.Sdds.Components.AiAnswerChipGroup.S", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.AiAnswerChipGroupS" + }, + { + "name": "xs", + "viewReference": "Serv.Sdds.Components.AiAnswerChipGroup.Xs", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.AiAnswerChipGroupXs" + } + ] + }, + { + "key": "icon-button", + "coreName": "IconButton", + "styleName": "IconButton", + "variations": [ + { + "name": "l", + "viewReference": "Serv.Sdds.Components.IconButton.L", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonL" + }, + { + "name": "l.default", + "viewReference": "Serv.Sdds.Components.IconButton.L.Default", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonLDefault" + }, + { + "name": "l.secondary", + "viewReference": "Serv.Sdds.Components.IconButton.L.Secondary", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonLSecondary" + }, + { + "name": "l.accent", + "viewReference": "Serv.Sdds.Components.IconButton.L.Accent", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonLAccent" + }, + { + "name": "l.positive", + "viewReference": "Serv.Sdds.Components.IconButton.L.Positive", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonLPositive" + }, + { + "name": "l.negative", + "viewReference": "Serv.Sdds.Components.IconButton.L.Negative", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonLNegative" + }, + { + "name": "l.warning", + "viewReference": "Serv.Sdds.Components.IconButton.L.Warning", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonLWarning" + }, + { + "name": "l.info", + "viewReference": "Serv.Sdds.Components.IconButton.L.Info", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonLInfo" + }, + { + "name": "m", + "viewReference": "Serv.Sdds.Components.IconButton.M", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonM" + }, + { + "name": "m.default", + "viewReference": "Serv.Sdds.Components.IconButton.M.Default", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonMDefault" + }, + { + "name": "m.secondary", + "viewReference": "Serv.Sdds.Components.IconButton.M.Secondary", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonMSecondary" + }, + { + "name": "m.accent", + "viewReference": "Serv.Sdds.Components.IconButton.M.Accent", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonMAccent" + }, + { + "name": "m.positive", + "viewReference": "Serv.Sdds.Components.IconButton.M.Positive", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonMPositive" + }, + { + "name": "m.negative", + "viewReference": "Serv.Sdds.Components.IconButton.M.Negative", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonMNegative" + }, + { + "name": "m.warning", + "viewReference": "Serv.Sdds.Components.IconButton.M.Warning", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonMWarning" + }, + { + "name": "m.info", + "viewReference": "Serv.Sdds.Components.IconButton.M.Info", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonMInfo" + }, + { + "name": "s", + "viewReference": "Serv.Sdds.Components.IconButton.S", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonS" + }, + { + "name": "s.default", + "viewReference": "Serv.Sdds.Components.IconButton.S.Default", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonSDefault" + }, + { + "name": "s.secondary", + "viewReference": "Serv.Sdds.Components.IconButton.S.Secondary", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonSSecondary" + }, + { + "name": "s.accent", + "viewReference": "Serv.Sdds.Components.IconButton.S.Accent", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonSAccent" + }, + { + "name": "s.positive", + "viewReference": "Serv.Sdds.Components.IconButton.S.Positive", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonSPositive" + }, + { + "name": "s.negative", + "viewReference": "Serv.Sdds.Components.IconButton.S.Negative", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonSNegative" + }, + { + "name": "s.warning", + "viewReference": "Serv.Sdds.Components.IconButton.S.Warning", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonSWarning" + }, + { + "name": "s.info", + "viewReference": "Serv.Sdds.Components.IconButton.S.Info", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonSInfo" + }, + { + "name": "xs", + "viewReference": "Serv.Sdds.Components.IconButton.Xs", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonXs" + }, + { + "name": "xs.default", + "viewReference": "Serv.Sdds.Components.IconButton.Xs.Default", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonXsDefault" + }, + { + "name": "xs.secondary", + "viewReference": "Serv.Sdds.Components.IconButton.Xs.Secondary", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonXsSecondary" + }, + { + "name": "xs.accent", + "viewReference": "Serv.Sdds.Components.IconButton.Xs.Accent", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonXsAccent" + }, + { + "name": "xs.positive", + "viewReference": "Serv.Sdds.Components.IconButton.Xs.Positive", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonXsPositive" + }, + { + "name": "xs.negative", + "viewReference": "Serv.Sdds.Components.IconButton.Xs.Negative", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonXsNegative" + }, + { + "name": "xs.warning", + "viewReference": "Serv.Sdds.Components.IconButton.Xs.Warning", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonXsWarning" + }, + { + "name": "xs.info", + "viewReference": "Serv.Sdds.Components.IconButton.Xs.Info", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.IconButtonXsInfo" + } + ] + }, + { + "key": "button-group", + "coreName": "ButtonGroup", + "styleName": "EmbeddedIconButtonGroup", + "variations": [ + { + "name": "xs", + "viewReference": "Serv.Sdds.Components.EmbeddedIconButtonGroup.Xs", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.EmbeddedIconButtonGroupXs" + }, + { + "name": "xs.wide", + "viewReference": "Serv.Sdds.Components.EmbeddedIconButtonGroup.Xs.Wide", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.EmbeddedIconButtonGroupXsWide" + }, + { + "name": "xs.dense", + "viewReference": "Serv.Sdds.Components.EmbeddedIconButtonGroup.Xs.Dense", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.EmbeddedIconButtonGroupXsDense" + }, + { + "name": "xs.no-gap", + "viewReference": "Serv.Sdds.Components.EmbeddedIconButtonGroup.Xs.NoGap", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.EmbeddedIconButtonGroupXsNoGap" + }, + { + "name": "s", + "viewReference": "Serv.Sdds.Components.EmbeddedIconButtonGroup.S", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.EmbeddedIconButtonGroupS" + }, + { + "name": "s.wide", + "viewReference": "Serv.Sdds.Components.EmbeddedIconButtonGroup.S.Wide", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.EmbeddedIconButtonGroupSWide" + }, + { + "name": "s.dense", + "viewReference": "Serv.Sdds.Components.EmbeddedIconButtonGroup.S.Dense", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.EmbeddedIconButtonGroupSDense" + }, + { + "name": "s.no-gap", + "viewReference": "Serv.Sdds.Components.EmbeddedIconButtonGroup.S.NoGap", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.EmbeddedIconButtonGroupSNoGap" + }, + { + "name": "m", + "viewReference": "Serv.Sdds.Components.EmbeddedIconButtonGroup.M", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.EmbeddedIconButtonGroupM" + }, + { + "name": "m.wide", + "viewReference": "Serv.Sdds.Components.EmbeddedIconButtonGroup.M.Wide", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.EmbeddedIconButtonGroupMWide" + }, + { + "name": "m.dense", + "viewReference": "Serv.Sdds.Components.EmbeddedIconButtonGroup.M.Dense", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.EmbeddedIconButtonGroupMDense" + }, + { + "name": "m.no-gap", + "viewReference": "Serv.Sdds.Components.EmbeddedIconButtonGroup.M.NoGap", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.EmbeddedIconButtonGroupMNoGap" + }, + { + "name": "l", + "viewReference": "Serv.Sdds.Components.EmbeddedIconButtonGroup.L", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.EmbeddedIconButtonGroupL" + }, + { + "name": "l.wide", + "viewReference": "Serv.Sdds.Components.EmbeddedIconButtonGroup.L.Wide", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.EmbeddedIconButtonGroupLWide" + }, + { + "name": "l.dense", + "viewReference": "Serv.Sdds.Components.EmbeddedIconButtonGroup.L.Dense", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.EmbeddedIconButtonGroupLDense" + }, + { + "name": "l.no-gap", + "viewReference": "Serv.Sdds.Components.EmbeddedIconButtonGroup.L.NoGap", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.EmbeddedIconButtonGroupLNoGap" + } + ] + }, + { + "key": "button-group", + "coreName": "ButtonGroup", + "styleName": "AiAnswerIconButtonGroup", + "variations": [ + { + "name": "xs", + "viewReference": "Serv.Sdds.Components.AiAnswerIconButtonGroup.Xs", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.AiAnswerIconButtonGroupXs" + }, + { + "name": "s", + "viewReference": "Serv.Sdds.Components.AiAnswerIconButtonGroup.S", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.AiAnswerIconButtonGroupS" + }, + { + "name": "m", + "viewReference": "Serv.Sdds.Components.AiAnswerIconButtonGroup.M", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.AiAnswerIconButtonGroupM" + }, + { + "name": "l", + "viewReference": "Serv.Sdds.Components.AiAnswerIconButtonGroup.L", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.AiAnswerIconButtonGroupL" + } + ] + }, + { + "key": "button-group", + "coreName": "ButtonGroup", + "styleName": "AiAnswerBasicButtonGroup", + "variations": [ + { + "name": "xs", + "viewReference": "Serv.Sdds.Components.AiAnswerBasicButtonGroup.Xs", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.AiAnswerBasicButtonGroupXs" + }, + { + "name": "s", + "viewReference": "Serv.Sdds.Components.AiAnswerBasicButtonGroup.S", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.AiAnswerBasicButtonGroupS" + }, + { + "name": "m", + "viewReference": "Serv.Sdds.Components.AiAnswerBasicButtonGroup.M", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.AiAnswerBasicButtonGroupM" + }, + { + "name": "l", + "viewReference": "Serv.Sdds.Components.AiAnswerBasicButtonGroup.L", + "viewOverlayReference": "Serv.Sdds.ComponentOverlays.AiAnswerBasicButtonGroupL" + } + ] } ] } \ No newline at end of file diff --git a/tokens/sdds.serv.view/.sdds/theme-info-view-system.json b/tokens/sdds.serv.view/.sdds/theme-info-view-system.json new file mode 100644 index 0000000000..a29adc87b5 --- /dev/null +++ b/tokens/sdds.serv.view/.sdds/theme-info-view-system.json @@ -0,0 +1,6 @@ +{ + "name": "Sdds", + "version": "0.9.0", + "platform": "vs", + "tokens": [] +} \ No newline at end of file diff --git a/tokens/sdds.serv.view/build.gradle.kts b/tokens/sdds.serv.view/build.gradle.kts index 0b4644b4af..bf5b7b9de3 100644 --- a/tokens/sdds.serv.view/build.gradle.kts +++ b/tokens/sdds.serv.view/build.gradle.kts @@ -11,7 +11,7 @@ import utils.themeVersion @Suppress("DSL_SCOPE_VIOLATION") plugins { id("convention.android-lib") - id(libs.plugins.themebuilder.get().pluginId) + id(libs.plugins.dsbuilder.get().pluginId) id("convention.maven-publish") id("convention.auto-bump") id("convention.testing") @@ -24,19 +24,23 @@ android { resourcePrefix = themeResPrefix } -themeBuilder { - themeSource(name = themeName, version = themeVersion, alias = themeAlias) - componentSource(name = componentsName, version = componentsVersion, alias = themeAlias) - view{ +dsBuilder { + autoGenerate.set(false) + view { themeParents { materialComponentsTheme() } setupShapeAppearance(sddsShape()) } - ktPackage("com.sdds.serv") - autoGenerate(false) - mode(THEME) - outputLocation(SRC) + packageName.set("com.sdds.serv") + outputLocation.set(SRC) + theme { + source(name = themeName, version = themeVersion, alias = themeAlias) + mode.set(THEME) + } + components { + source(name = componentsName, version = componentsVersion, alias = themeAlias) + } } dependencies { diff --git a/tokens/sdds.serv.view/docs/build.gradle.kts b/tokens/sdds.serv.view/docs/build.gradle.kts index 42f701f3a0..790d96e471 100644 --- a/tokens/sdds.serv.view/docs/build.gradle.kts +++ b/tokens/sdds.serv.view/docs/build.gradle.kts @@ -1,6 +1,12 @@ +import extensions.docs.DocusaurusExtension + @Suppress("DSL_SCOPE_VIOLATION") plugins { - id("convention.documentation-view") + id("convention.android-lib") + id("convention.integration-detekt") + id("convention.docusaurus") + id("com.google.devtools.ksp") + id(libs.plugins.dsbuilder.get().pluginId) id("convention.testing") alias(libs.plugins.roborazzi) } @@ -9,8 +15,32 @@ android { namespace = "com.sdds.serv.docs" } +ksp { + arg("packageName", "com.sdds.serv.docs") +} + +dsBuilder { + autoGenerate.set(false) + documentation { + view() + } +} + +extensions.configure("docusaurus") { + components.set(layout.projectDirectory.file("../.sdds/config-info-view-system.json")) + snippetsDir.set(layout.projectDirectory.dir("../.sdds/temp/docs/assets/examples")) +} + +tasks.named("docusaurusGenerate") { + dependsOn("documentationAggregate") +} + dependencies { + "sddsCoreDocumentation"("integration-core:uikit-fixtures:unspecified:docs@jar") + implementation("sdds-core:docs") + ksp("sdds-core:docs") implementation(project(":sdds.serv.view")) + testImplementation("integration-core:uikit-fixtures") implementation(libs.sdds.uikit) implementation(libs.base.androidX.core) implementation(libs.base.androidX.appcompat) diff --git a/tokens/sdds.serv.view/integration/build.gradle.kts b/tokens/sdds.serv.view/integration/build.gradle.kts index caad71c283..06d8a3e5ac 100644 --- a/tokens/sdds.serv.view/integration/build.gradle.kts +++ b/tokens/sdds.serv.view/integration/build.gradle.kts @@ -1,7 +1,21 @@ +import com.sdds.plugin.themebuilder.OutputLocation + @Suppress("DSL_SCOPE_VIOLATION") plugins { id("convention.android-lib") - id("convention.integration-view") + id("convention.integration-detekt") + id(libs.plugins.dsbuilder.get().pluginId) +} + +dsBuilder { + outputLocation.set(OutputLocation.SRC) + autoGenerate.set(false) + sandbox { + view { + generatedPackageName.set("com.sdds.serv.integration") + themeAlias.set("Sdds") + } + } } android { diff --git a/tokens/sdds.serv.view/integration/gradle.properties b/tokens/sdds.serv.view/integration/gradle.properties deleted file mode 100644 index b2e5569624..0000000000 --- a/tokens/sdds.serv.view/integration/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -integration.view.config-path=../config-info-view-system.json -integration.view.package-name=com.sdds.serv.integration \ No newline at end of file