diff --git a/Sources/Publish/Internal/HTMLGenerator.swift b/Sources/Publish/Internal/HTMLGenerator.swift
index 411bf428..8683c722 100644
--- a/Sources/Publish/Internal/HTMLGenerator.swift
+++ b/Sources/Publish/Internal/HTMLGenerator.swift
@@ -15,32 +15,32 @@ internal struct HTMLGenerator {
let context: PublishingContext
func generate() async throws {
- try await withThrowingTaskGroup(of: Void.self) { group in
- group.addTask { try await copyThemeResources() }
- group.addTask { try generateIndexHTML() }
- group.addTask { try await generateSectionHTML() }
- group.addTask { try await generatePageHTML() }
- group.addTask { try await generateTagHTMLIfNeeded() }
-
- // Throw any errors generated by the above set of operations:
- for try await _ in group {}
+ try await withThrowingTaskGroup(of: (substep: String, paths: [Path]).self) { group in
+ group.addTask { (substep: "Copy theme resources", paths: try await copyThemeResources()) }
+ group.addTask { (substep: "Generate index", paths: try generateIndexHTML()) }
+ group.addTask { (substep: "Generate sections", paths: try await generateSectionHTML()) }
+ group.addTask { (substep: "Generate pages", paths: try await generatePageHTML()) }
+ group.addTask { (substep: "Generate tags", paths: try await generateTagHTMLIfNeeded()) }
+
+ try await validate(group)
}
}
}
private extension HTMLGenerator {
- func copyThemeResources() async throws {
+ func copyThemeResources() async throws -> [Path] {
guard !theme.resourcePaths.isEmpty else {
- return
+ return []
}
let creationFile = try File(path: theme.creationPath.string)
let packageFolder = try creationFile.resolveSwiftPackageFolder()
- try await theme.resourcePaths.concurrentForEach { path in
+ return try await theme.resourcePaths.concurrentMap { path -> Path in
do {
let file = try packageFolder.file(at: path.string)
try context.copyFileToOutput(file, targetFolderPath: nil)
+ return path
} catch {
throw PublishingError(
path: path,
@@ -51,22 +51,27 @@ private extension HTMLGenerator {
}
}
- func generateIndexHTML() throws {
+ func generateIndexHTML() throws -> [Path] {
let html = try theme.makeIndexHTML(context.index, context)
- let indexFile = try context.createOutputFile(at: "index.html")
+ let path = Path("index.html")
+ let indexFile = try context.createOutputFile(at: path)
try indexFile.write(html.render(indentedBy: indentation))
+ return [path]
}
- func generateSectionHTML() async throws {
- try await context.sections.concurrentForEach { section in
- try outputHTML(
+ func generateSectionHTML() async throws -> [Path] {
+ try await context.sections.concurrentFlatMap { section -> [Path] in
+ var allPaths = [Path]()
+
+ let sectionPath = try outputHTML(
for: section,
indentedBy: indentation,
using: theme.makeSectionHTML,
fileMode: .foldersAndIndexFiles
)
-
- try await section.items.concurrentForEach { item in
+ allPaths.append(sectionPath)
+
+ let sectionItemPaths = try await section.items.concurrentMap { item -> Path in
try outputHTML(
for: item,
indentedBy: indentation,
@@ -74,11 +79,14 @@ private extension HTMLGenerator {
fileMode: fileMode
)
}
+
+ allPaths.append(contentsOf: sectionItemPaths)
+ return allPaths
}
}
- func generatePageHTML() async throws {
- try await context.pages.values.concurrentForEach { page in
+ func generatePageHTML() async throws -> [Path] {
+ try await context.pages.values.concurrentMap { page -> Path in
try outputHTML(
for: page,
indentedBy: indentation,
@@ -88,9 +96,9 @@ private extension HTMLGenerator {
}
}
- func generateTagHTMLIfNeeded() async throws {
+ func generateTagHTMLIfNeeded() async throws -> [Path] {
guard let config = context.site.tagHTMLConfig else {
- return
+ return []
}
let listPage = TagListPage(
@@ -99,13 +107,15 @@ private extension HTMLGenerator {
content: config.listContent ?? .init()
)
+ var allPaths = [Path]()
if let listHTML = try theme.makeTagListHTML(listPage, context) {
let listPath = Path("\(config.basePath)/index.html")
let listFile = try context.createOutputFile(at: listPath)
try listFile.write(listHTML.render(indentedBy: indentation))
+ allPaths.append(listPath)
}
- try await context.allTags.concurrentForEach { tag in
+ let tagPaths: [Path] = try await context.allTags.concurrentCompactMap { tag -> Path? in
let detailsPath = context.site.path(for: tag)
let detailsContent = config.detailsContentResolver(tag)
@@ -116,16 +126,35 @@ private extension HTMLGenerator {
)
guard let detailsHTML = try theme.makeTagDetailsHTML(detailsPage, context) else {
- return
+ return nil
}
- try outputHTML(
+ return try outputHTML(
for: detailsPage,
indentedBy: indentation,
using: { _, _ in detailsHTML },
fileMode: fileMode
)
}
+
+ allPaths.append(contentsOf: tagPaths)
+ return allPaths
+ }
+
+ func validate(_ group: ThrowingTaskGroup<(substep: String, paths: [Path]), Error>) async throws {
+ var pathSubsteps = [Path: [String]]()
+ for try await substepAndPaths in group {
+ for path in substepAndPaths.paths {
+ if let previousSubsteps = pathSubsteps[path] {
+ let substeps = previousSubsteps.appending(substepAndPaths.substep)
+ throw PublishingError(
+ path: path,
+ infoMessage: "Path conflict in substeps: \(substeps)"
+ )
+ }
+ pathSubsteps[path, default: []].append(substepAndPaths.substep)
+ }
+ }
}
func outputHTML(
@@ -133,11 +162,12 @@ private extension HTMLGenerator {
indentedBy indentation: Indentation.Kind?,
using generator: (T, PublishingContext) throws -> HTML,
fileMode: HTMLFileMode
- ) throws {
+ ) throws -> Path {
let html = try generator(location, context)
let path = filePath(for: location, fileMode: fileMode)
let file = try context.createOutputFile(at: path)
try file.write(html.render(indentedBy: indentation))
+ return path
}
func filePath(for location: Location, fileMode: HTMLFileMode) -> Path {
diff --git a/Tests/PublishTests/Tests/HTMLGenerationTests.swift b/Tests/PublishTests/Tests/HTMLGenerationTests.swift
index 2afca997..6065ea09 100644
--- a/Tests/PublishTests/Tests/HTMLGenerationTests.swift
+++ b/Tests/PublishTests/Tests/HTMLGenerationTests.swift
@@ -282,6 +282,31 @@ final class HTMLGenerationTests: PublishTestCase {
)
}
+ func testGeneratingConflictingFilesThrowsError() throws {
+ let folder = try Folder.createTemporary()
+
+ var thrownError: PublishingError?
+ do {
+ try publishWebsite(
+ in: folder,
+ using: [
+ .addMarkdownFiles(),
+ .generateHTML(withTheme: .foundation)
+ ],
+ content: [
+ // This file has the same name as the `WebsiteStub.SectionID.one` case, which
+ // causes multiple outputs at the same location.
+ "one.md": "# One content",
+ ]
+ )
+ } catch {
+ thrownError = error as? PublishingError
+ }
+
+ let path = try require(thrownError?.path)
+ XCTAssertEqual(path, "one/index.html")
+ }
+
func testFoundationTheme() throws {
let folder = try Folder.createTemporary()