Skip to content

Commit d0e7bd8

Browse files
committed
Introduce pkl-doc model version 2
Currently, in order to update a pkl-doc documentation site, almost the entire existing site is read in order to update metadata like known versions, known subtypes, and more. For example, adding a new version of a package requires that the existing runtime data of all existing versions be updated. Eventually, this causes the required storage size to balloon exponentially to the number of versions. This addresses these limitations by: 1. Updating the runtime data structure to move "known versions" metadata to the package level (the same JSON file is used for all versions). 2. Eliminating known subtype and known usage information at a cross-package level. 3. Generating the search index by consuming the previously generated search index. 4. Generating the main page by consuming the search index. Because this changes how runtime data is stored, an existing docsite needs to be migrated. This also introduces a new migration command, `pkl-doc --migrate`, which transforms an older version of the website into a newer version.
1 parent 78ba6bf commit d0e7bd8

File tree

22 files changed

+1367
-491
lines changed

22 files changed

+1367
-491
lines changed

pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliException.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,13 @@ open class CliException(
2626
*/
2727
message: String,
2828

29+
/** The cause */
30+
cause: Throwable?,
31+
2932
/** The process exit code to use. */
3033
val exitCode: Int = 1,
31-
) : RuntimeException(message) {
34+
) : RuntimeException(message, cause) {
35+
constructor(message: String, exitCode: Int = 1) : this(message, null, exitCode)
3236

3337
override fun toString(): String = message!!
3438
}
@@ -41,7 +45,11 @@ class CliBugException(
4145
/** The process exit code to use. */
4246
exitCode: Int = 1,
4347
) :
44-
CliException("An unexpected error has occurred. Would you mind filing a bug report?", exitCode) {
48+
CliException(
49+
"An unexpected error has occurred. Would you mind filing a bug report?",
50+
theCause,
51+
exitCode,
52+
) {
4553

4654
override fun toString(): String = "$message\n\n${theCause.printStackTraceToString()}"
4755
}

pkl-doc/src/main/kotlin/org/pkl/doc/CliDocGenerator.kt

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import org.pkl.core.packages.*
3434
* For the low-level Pkldoc API, see [DocGenerator].
3535
*/
3636
class CliDocGenerator(private val options: CliDocGeneratorOptions) : CliCommand(options.base) {
37-
3837
private val packageResolver =
3938
PackageResolver.getInstance(securityManager, httpClient, moduleCacheDir)
4039

@@ -60,6 +59,17 @@ class CliDocGenerator(private val options: CliDocGeneratorOptions) : CliCommand(
6059
),
6160
)
6261

62+
private val versions = mutableMapOf<String, Version>()
63+
64+
private val versionComparator =
65+
Comparator<String> { v1, v2 ->
66+
versions
67+
.getOrPut(v1) { Version.parse(v1) }
68+
.compareTo(versions.getOrPut(v2) { Version.parse(v2) })
69+
}
70+
71+
private val docMigrator = DocMigrator(options.outputDir, System.out, versionComparator)
72+
6373
private fun DependencyMetadata.getPackageDependencies(): List<DocPackageInfo.PackageDependency> {
6474
return buildList {
6575
for ((_, dependency) in dependencies) {
@@ -87,14 +97,12 @@ class CliDocGenerator(private val options: CliDocGeneratorOptions) : CliCommand(
8797
}
8898

8999
private fun PackageUri.toDocPackageInfo(): DocPackageInfo {
90-
val metadataAndChecksum =
100+
val (metadata, checksum) =
91101
try {
92102
packageResolver.getDependencyMetadataAndComputeChecksum(this)
93103
} catch (e: PackageLoadError) {
94104
throw CliException("Failed to package metadata for $this: ${e.message}")
95105
}
96-
val metadata = metadataAndChecksum.first
97-
val checksum = metadataAndChecksum.second
98106
return DocPackageInfo(
99107
name = "${uri.authority}${uri.path.substringBeforeLast('@')}",
100108
moduleNamePrefix = "${metadata.name}.",
@@ -130,6 +138,15 @@ class CliDocGenerator(private val options: CliDocGeneratorOptions) : CliCommand(
130138
}
131139

132140
override fun doRun() {
141+
if (options.migrate) {
142+
docMigrator.run()
143+
return
144+
}
145+
if (!docMigrator.isUpToDate) {
146+
throw CliException(
147+
"pkldoc website model is too old (found: ${docMigrator.docsiteVersion}, required: ${DocMigrator.CURRENT_VERSION}). Run `pkldoc --migrate` to migrate the website."
148+
)
149+
}
133150
val docsiteInfoModuleUris = mutableListOf<URI>()
134151
val packageInfoModuleUris = mutableListOf<URI>()
135152
val regularModuleUris = mutableListOf<URI>()
@@ -271,10 +288,11 @@ class CliDocGenerator(private val options: CliDocGeneratorOptions) : CliCommand(
271288
options.normalizedOutputDir,
272289
options.isTestMode,
273290
options.noSymlinks,
291+
docMigrator,
274292
)
275293
.run()
276294
} catch (e: DocGeneratorException) {
277-
throw CliException(e.message!!)
295+
throw CliException(e.message!!, e)
278296
}
279297
}
280298
}

pkl-doc/src/main/kotlin/org/pkl/doc/CliDocGeneratorOptions.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ constructor(
4747
* however, will create a full copy instead.
4848
*/
4949
val noSymlinks: Boolean = false,
50+
51+
/** Migrate existing pkldoc */
52+
val migrate: Boolean = false,
5053
) {
5154
/** [outputDir] after undergoing normalization. */
5255
val normalizedOutputDir: Path = base.normalizedWorkingDir.resolveSafely(outputDir)

pkl-doc/src/main/kotlin/org/pkl/doc/DocGenerator.kt

Lines changed: 67 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package org.pkl.doc
1717

1818
import java.io.IOException
1919
import java.net.URI
20+
import java.nio.file.Files
2021
import java.nio.file.Path
2122
import kotlin.io.path.*
2223
import org.pkl.commons.copyRecursively
@@ -39,11 +40,11 @@ class DocGenerator(
3940
*/
4041
private val docsiteInfo: DocsiteInfo,
4142

42-
/** The modules to generate documentation for, grouped by package. */
43-
modules: Map<DocPackageInfo, Collection<ModuleSchema>>,
43+
/** The packages to generate documentation for. */
44+
packages: Map<DocPackageInfo, Collection<ModuleSchema>>,
4445

4546
/**
46-
* A function to resolve imports in [modules] and [docsiteInfo].
47+
* A function to resolve imports in [packages] and [docsiteInfo].
4748
*
4849
* Module `pkl.base` is resolved with this function even if not explicitly imported.
4950
*/
@@ -73,16 +74,23 @@ class DocGenerator(
7374
* however, will create a full copy instead.
7475
*/
7576
private val noSymlinks: Boolean = false,
77+
78+
/**
79+
* The doc migrator that is used to determine the latest docsite version, as well as update the
80+
* version file.
81+
*/
82+
private val docMigrator: DocMigrator = DocMigrator(outputDir, System.out, versionComparator),
7683
) {
7784
companion object {
7885
const val CURRENT_DIRECTORY_NAME = "current"
7986

80-
internal fun List<PackageData>.current(
81-
versionComparator: Comparator<String>
87+
internal fun determineCurrentPackages(
88+
packages: List<PackageData>,
89+
descendingVersionComparator: Comparator<String>,
8290
): List<PackageData> {
8391
val comparator =
84-
compareBy<PackageData> { it.ref.pkg }.thenBy(versionComparator) { it.ref.version }
85-
return asSequence()
92+
compareBy<PackageData> { it.ref.pkg }.thenBy(descendingVersionComparator) { it.ref.version }
93+
return packages
8694
// If matching a semver pattern, remove any version that has a prerelease
8795
// version (e.g. SNAPSHOT in 1.2.3-SNAPSHOT)
8896
.filter { Version.parseOrNull(it.ref.version)?.preRelease == null }
@@ -94,46 +102,82 @@ class DocGenerator(
94102

95103
private val descendingVersionComparator: Comparator<String> = versionComparator.reversed()
96104

97-
private val docPackages: List<DocPackage> = modules.map { DocPackage(it.key, it.value.toList()) }
105+
private val docPackages: List<DocPackage> =
106+
packages.map { DocPackage(it.key, it.value.toList()) }.filter { !it.isUnlisted }
107+
108+
private fun getCurrentPackages(siteSearchIndex: List<SearchIndexGenerator.PackageIndexEntry>) =
109+
buildList {
110+
for (entry in siteSearchIndex) {
111+
var packageDataFile =
112+
outputDir.resolve(entry.packageEntry.url).resolveSibling("package-data.json")
113+
if (!Files.exists(packageDataFile)) {
114+
// search-index.js in Pkl 0.29 and below did not encode path.
115+
// If we get a file does not exist, try again by encoding the path.
116+
packageDataFile =
117+
outputDir
118+
.resolve(entry.packageEntry.url.pathEncoded)
119+
.resolveSibling("package-data.json")
120+
if (!Files.exists((packageDataFile))) {
121+
println(
122+
"[Warn] likely corrupted search index; missing $packageDataFile. This entry will be removed in the newly generated index."
123+
)
124+
continue
125+
}
126+
}
127+
add(PackageData.read(packageDataFile))
128+
}
129+
}
98130

99131
/** Runs this documentation generator. */
100132
fun run() {
101133
try {
134+
if (!docMigrator.isUpToDate) {
135+
throw DocGeneratorException(
136+
"Docsite is not up to date. Expected: ${DocMigrator.CURRENT_VERSION}. Found: ${docMigrator.docsiteVersion}. Use DocMigrator to migrate the site."
137+
)
138+
}
102139
val htmlGenerator =
103140
HtmlGenerator(docsiteInfo, docPackages, importResolver, outputDir, isTestMode)
104141
val searchIndexGenerator = SearchIndexGenerator(outputDir)
105142
val packageDataGenerator = PackageDataGenerator(outputDir)
106143
val runtimeDataGenerator = RuntimeDataGenerator(descendingVersionComparator, outputDir)
107144

108145
for (docPackage in docPackages) {
109-
if (docPackage.isUnlisted) continue
110-
111146
docPackage.deletePackageDir()
112147
htmlGenerator.generate(docPackage)
113148
searchIndexGenerator.generate(docPackage)
114149
packageDataGenerator.generate(docPackage)
115150
}
116151

117-
val packagesData = packageDataGenerator.readAll()
118-
val currentPackagesData = packagesData.current(descendingVersionComparator)
119-
120-
createCurrentDirectories(currentPackagesData)
121-
122-
htmlGenerator.generateSite(currentPackagesData)
123-
searchIndexGenerator.generateSiteIndex(currentPackagesData)
124-
runtimeDataGenerator.deleteDataDir()
125-
runtimeDataGenerator.generate(packagesData)
152+
val newlyGeneratedPackages = docPackages.map(::PackageData).sortedBy { it.ref.pkg }
153+
val currentSearchIndex = searchIndexGenerator.getCurrentSearchIndex()
154+
val existingCurrentPackages = getCurrentPackages(currentSearchIndex)
155+
val currentPackages =
156+
determineCurrentPackages(
157+
newlyGeneratedPackages + existingCurrentPackages,
158+
descendingVersionComparator,
159+
)
160+
161+
createCurrentDirectories(currentPackages, existingCurrentPackages)
162+
searchIndexGenerator.generateSiteIndex(currentPackages)
163+
htmlGenerator.generateSite(currentPackages)
164+
runtimeDataGenerator.generate(newlyGeneratedPackages)
165+
docMigrator.updateDocsiteVersion()
126166
} catch (e: IOException) {
127-
throw DocGeneratorException("I/O error generating documentation.", e)
167+
throw DocGeneratorException("I/O error generating documentation: ${e.message}", e)
128168
}
129169
}
130170

131171
private fun DocPackage.deletePackageDir() {
132172
outputDir.resolve(IoUtils.encodePath("$name/$version")).deleteRecursively()
133173
}
134174

135-
private fun createCurrentDirectories(currentPackagesData: List<PackageData>) {
136-
for (packageData in currentPackagesData) {
175+
private fun createCurrentDirectories(
176+
currentPackages: List<PackageData>,
177+
existingCurrentPackages: List<PackageData>,
178+
) {
179+
val packagesToCreate = currentPackages - existingCurrentPackages
180+
for (packageData in packagesToCreate) {
137181
val basePath = outputDir.resolve(packageData.ref.pkg.pathEncoded)
138182
val src = basePath.resolve(packageData.ref.version)
139183
val dst = basePath.resolve(CURRENT_DIRECTORY_NAME)
@@ -218,7 +262,7 @@ internal class DocModule(
218262
get() = schema.moduleName
219263

220264
val path: String by lazy {
221-
name.substring(parent.docPackageInfo.moduleNamePrefix.length).replace('.', '/')
265+
name.pathEncoded.substring(parent.docPackageInfo.moduleNamePrefix.length).replace('.', '/')
222266
}
223267

224268
val overview: String?

0 commit comments

Comments
 (0)