From 389d1f8a2d682b7ba9c28d95fe87dcbdafc9d9ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20R=C3=B6nnqvist?= Date: Fri, 6 Dec 2024 08:52:20 +0100 Subject: [PATCH] Revert "Revert "Deprecate `FileSystemProvider` and `RenderNodeProvider`"" (#1117) --- .../Navigator/NavigatorIndex+Ext.swift | 1 + .../Indexing/Navigator/NavigatorIndex.swift | 74 ++++++++-- .../Workspace/FileSystemProvider.swift | 4 +- ...leSystemDataProvider+BundleDiscovery.swift | 1 + .../LocalFileSystemDataProvider.swift | 1 + .../FileManagerProtocol+FilesSequence.swift | 75 ++++++++++ .../Action/Actions/Convert/Indexer.swift | 2 +- .../Action/Actions/IndexAction.swift | 11 +- .../TransformForStaticHostingAction.swift | 3 +- .../IndexAction+CommandInitialization.swift | 8 +- .../ArgumentParsing/Subcommands/Index.swift | 6 +- .../StaticHostableTransformer.swift | 132 ++++-------------- .../ConvertActionTests.swift | 4 +- .../IndexActionTests.swift | 9 +- .../StaticHostableTransformerTests.swift | 8 +- .../Utility/TestFileSystemTests.swift | 22 +++ 16 files changed, 219 insertions(+), 142 deletions(-) create mode 100644 Sources/SwiftDocC/Utility/FileManagerProtocol+FilesSequence.swift diff --git a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex+Ext.swift b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex+Ext.swift index 9ca01bf78d..2ed5ea9d08 100644 --- a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex+Ext.swift +++ b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex+Ext.swift @@ -14,6 +14,7 @@ import Foundation This class provides a simple way to transform a `FileSystemProvider` into a `RenderNodeProvider` to feed an index builder. The data from the disk is fetched and processed in an efficient way to build a navigator index. */ +@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released.") public class FileSystemRenderNodeProvider: RenderNodeProvider { /// The internal `FileSystemProvider` reference. diff --git a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift index 1ec1ddddbf..503d4e0611 100644 --- a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift +++ b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift @@ -12,6 +12,7 @@ import Foundation import Crypto /// A protocol to provide data to be indexed. +@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released.") public protocol RenderNodeProvider { /// Get an instance of `RenderNode` to be processed by the index. /// - Note: Returning `nil` will end the indexing process. @@ -21,8 +22,6 @@ public protocol RenderNodeProvider { func getProblems() -> [Problem] } - - /** A `NavigatorIndex` contains all the necessary information to display the data inside a navigator. The data ranges from the tree to the necessary pieces of information to filter the content and perform actions in a fast way. @@ -479,8 +478,12 @@ extension NavigatorIndex { open class Builder { /// The data provider. + @available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") public let renderNodeProvider: RenderNodeProvider? + /// The documentation archive to build an index from. + public let archiveURL: URL? + /// The output URL. public let outputURL: URL @@ -554,7 +557,6 @@ extension NavigatorIndex { /// Indicates if the page title should be used instead of the navigator title. private let usePageTitle: Bool - /// Maps the icon render references in the navigator items created by this builder /// to their image references. /// @@ -562,18 +564,31 @@ extension NavigatorIndex { /// for any custom icons used in this navigator index. var iconReferences = [String : ImageReference]() - /// Create a new a builder with the given data provider and output URL. /// - Parameters: - /// - renderNodeProvider: The `RenderNode` provider to use. + /// - archiveURL: The location of the documentation archive that the builder builds an navigator index for. /// - outputURL: The location where the builder will write the the built navigator index. /// - bundleIdentifier: The bundle identifier of the documentation that the builder builds a navigator index for. /// - sortRootChildrenByName: Configure the builder to sort root's children by name. /// - groupByLanguage: Configure the builder to group the entries by language. /// - writePathsOnDisk: Configure the builder to write each navigator item's path components to the location. /// - usePageTitle: Configure the builder to use the "page title" instead of the "navigator title" as the title for each entry. + public init(archiveURL: URL? = nil, outputURL: URL, bundleIdentifier: String, sortRootChildrenByName: Bool = false, groupByLanguage: Bool = false, writePathsOnDisk: Bool = true, usePageTitle: Bool = false) { + self.archiveURL = archiveURL + self.renderNodeProvider = nil + self.outputURL = outputURL + self.bundleIdentifier = bundleIdentifier + self.sortRootChildrenByName = sortRootChildrenByName + self.groupByLanguage = groupByLanguage + self.writePathsOnDisk = writePathsOnDisk + self.usePageTitle = usePageTitle + } + + @available(*, deprecated, renamed: "init(archiveURL:outputURL:bundleIdentifier:sortRootChildrenByName:groupByLanguage:writePathsOnDisk:usePageTitle:)", message: "Use 'init(archiveURL:outputURL:bundleIdentifier:sortRootChildrenByName:groupByLanguage:writePathsOnDisk:usePageTitle:)' instead. This deprecated API will be removed after 6.2 is released") + @_disfavoredOverload public init(renderNodeProvider: RenderNodeProvider? = nil, outputURL: URL, bundleIdentifier: String, sortRootChildrenByName: Bool = false, groupByLanguage: Bool = false, writePathsOnDisk: Bool = true, usePageTitle: Bool = false) { self.renderNodeProvider = renderNodeProvider + self.archiveURL = nil self.outputURL = outputURL self.bundleIdentifier = bundleIdentifier self.sortRootChildrenByName = sortRootChildrenByName @@ -1245,13 +1260,43 @@ extension NavigatorIndex { problems.append(problem) } - /** - Build the index using the passed instance of `RenderNodeProvider` if available. - - Returns: A list containing all the problems encountered during indexing. - - Note: If a provider is not available, this method would generate a fatal error. - */ + + /// Build the index using the render nodes files in the provided documentation archive. + /// - Returns: A list containing all the errors encountered during indexing. + /// - Precondition: Either ``archiveURL`` or ``renderNodeProvider`` is set. public func build() -> [Problem] { - precondition(renderNodeProvider != nil, "Calling build without a renderNodeProvider set is not permitted.") + if let archiveURL { + return _build(archiveURL: archiveURL) + } else { + return (self as _DeprecatedRenderNodeProviderAccess)._legacyBuild() + } + } + + // After 6.2 is released, move this into `build()`. + private func _build(archiveURL: URL) -> [Problem] { + setup() + + let dataDirectory = archiveURL.appendingPathComponent(NodeURLGenerator.Path.dataFolderName, isDirectory: true) + for file in FileManager.default.recursiveFiles(startingPoint: dataDirectory) where file.pathExtension.lowercased() == "json" { + do { + let data = try Data(contentsOf: file) + let renderNode = try RenderNode.decode(fromJSON: data) + try index(renderNode: renderNode) + } catch { + problems.append(error.problem(source: file, + severity: .warning, + summaryPrefix: "RenderNode indexing process failed")) + } + } + + finalize() + + return problems + } + + @available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") + fileprivate func _legacyBuild() -> [Problem] { + precondition(renderNodeProvider != nil, "Calling `build()` without an `archiveURL` or `renderNodeProvider` set is not permitted.") setup() @@ -1274,7 +1319,6 @@ extension NavigatorIndex { return availabilityIDs[Int(availabilityID)] } } - } fileprivate extension Error { @@ -1343,3 +1387,9 @@ enum PathHasher: String { } } } + +private protocol _DeprecatedRenderNodeProviderAccess { + // This private function accesses the deprecated RenderNodeProvider + func _legacyBuild() -> [Problem] +} +extension NavigatorIndex.Builder: _DeprecatedRenderNodeProviderAccess {} diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/FileSystemProvider.swift b/Sources/SwiftDocC/Infrastructure/Workspace/FileSystemProvider.swift index dc0d5ad148..085343cea6 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/FileSystemProvider.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/FileSystemProvider.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2024 Apple Inc. and the Swift project authors Licensed under Apache License v2.0 with Runtime Library Exception See https://swift.org/LICENSE.txt for license information @@ -11,12 +11,14 @@ import Foundation /// A type that vends a tree of virtual filesystem objects. +@available(*, deprecated, message: "Use 'FileManagerProtocol.recursiveFiles(startingPoint:)' instead. This deprecated API will be removed after 6.2 is released.") public protocol FileSystemProvider { /// The organization of the files that this provider provides. var fileSystem: FSNode { get } } /// An element in a virtual filesystem. +@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released.") public enum FSNode { /// A file in a filesystem. case file(File) diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider+BundleDiscovery.swift b/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider+BundleDiscovery.swift index 7eff159b16..9101f51ec1 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider+BundleDiscovery.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider+BundleDiscovery.swift @@ -142,6 +142,7 @@ extension LocalFileSystemDataProvider: DocumentationWorkspaceDataProvider { } } +@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released.") fileprivate extension [FSNode] { /// Returns the first file that matches a given predicate. /// - Parameter predicate: A closure that takes a file as its argument and returns a Boolean value indicating whether the file should be returned from this function. diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider.swift b/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider.swift index 568c3f9e70..d0ea8e313b 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider.swift @@ -11,6 +11,7 @@ import Foundation /// A type that provides documentation bundles that it discovers by traversing the local file system. +@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released.") public struct LocalFileSystemDataProvider: FileSystemProvider { public var identifier: String = UUID().uuidString diff --git a/Sources/SwiftDocC/Utility/FileManagerProtocol+FilesSequence.swift b/Sources/SwiftDocC/Utility/FileManagerProtocol+FilesSequence.swift new file mode 100644 index 0000000000..8cbe18b1cc --- /dev/null +++ b/Sources/SwiftDocC/Utility/FileManagerProtocol+FilesSequence.swift @@ -0,0 +1,75 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2024 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +import Foundation + +extension FileManagerProtocol { + /// Returns a sequence of all the files in the directory structure from the starting point. + /// - Parameters: + /// - startingPoint: The file or directory that's the top of the directory structure that the file manager traverses. + /// - options: Options for how the file manager enumerates the contents of directories. Defaults to `.skipsHiddenFiles`. + /// - Returns: A sequence of the files in the directory structure. + package func recursiveFiles(startingPoint: URL, options: FileManager.DirectoryEnumerationOptions = .skipsHiddenFiles) -> IteratorSequence<_FilesIterator> { + IteratorSequence(_FilesIterator(fileManager: self, startingPoint: startingPoint, options: options)) + } +} + +// FIXME: This should be private and `FileManagerProtocol.recursiveFiles(startingPoint:options:)` should return `some Sequence` +// but because of https://github.com/swiftlang/swift/issues/77955 it needs to be exposed as an explicit type to avoid a SIL Validation error in the Swift compiler. + +/// An iterator that traverses the directory structure and returns the files in breadth-first order. +package struct _FilesIterator: IteratorProtocol { + /// The file manager that the iterator uses to traverse the directory structure. + private var fileManager: any FileManagerProtocol // This can't be a generic because of https://github.com/swiftlang/swift/issues/77955 + private var options: FileManager.DirectoryEnumerationOptions + + private var foundFiles: [URL] + private var foundDirectories: [URL] + + fileprivate init(fileManager: any FileManagerProtocol, startingPoint: URL, options: FileManager.DirectoryEnumerationOptions) { + self.fileManager = fileManager + self.options = options + + // Check if the starting point is a file or a directory. + if fileManager.directoryExists(atPath: startingPoint.path) { + foundFiles = [] + foundDirectories = [startingPoint] + } else { + foundFiles = [startingPoint] + foundDirectories = [] + } + } + + package mutating func next() -> URL? { + // If the iterator has already found some files, return those first + if !foundFiles.isEmpty { + return foundFiles.removeFirst() + } + + // Otherwise, check the next found directory and add its contents + guard !foundDirectories.isEmpty else { + // Traversed the entire directory structure + return nil + } + + let directory = foundDirectories.removeFirst() + guard let (newFiles, newDirectories) = try? fileManager.contentsOfDirectory(at: directory, options: options) else { + // The iterator protocol doesn't have a mechanism for raising errors. If an error occurs we + return nil + } + + foundFiles.append(contentsOf: newFiles) + foundDirectories.append(contentsOf: newDirectories) + + // Iterate again after adding new found files and directories. + // This enables the iterator do recurse multiple layers of directories until it finds a file. + return next() + } +} diff --git a/Sources/SwiftDocCUtilities/Action/Actions/Convert/Indexer.swift b/Sources/SwiftDocCUtilities/Action/Actions/Convert/Indexer.swift index c8be4857f3..1097395d89 100644 --- a/Sources/SwiftDocCUtilities/Action/Actions/Convert/Indexer.swift +++ b/Sources/SwiftDocCUtilities/Action/Actions/Convert/Indexer.swift @@ -36,7 +36,7 @@ extension ConvertAction { init(outputURL: URL, bundleID: DocumentationBundle.Identifier) throws { let indexURL = outputURL.appendingPathComponent("index", isDirectory: true) indexBuilder = Synchronized( - NavigatorIndex.Builder(renderNodeProvider: nil, + NavigatorIndex.Builder( outputURL: indexURL, bundleIdentifier: bundleID.rawValue, sortRootChildrenByName: true, diff --git a/Sources/SwiftDocCUtilities/Action/Actions/IndexAction.swift b/Sources/SwiftDocCUtilities/Action/Actions/IndexAction.swift index 4062acc4ce..b1cd729aac 100644 --- a/Sources/SwiftDocCUtilities/Action/Actions/IndexAction.swift +++ b/Sources/SwiftDocCUtilities/Action/Actions/IndexAction.swift @@ -13,21 +13,21 @@ import SwiftDocC /// An action that creates an index of a documentation bundle. public struct IndexAction: AsyncAction { - let rootURL: URL + let archiveURL: URL let outputURL: URL let bundleIdentifier: String var diagnosticEngine: DiagnosticEngine /// Initializes the action with the given validated options, creates or uses the given action workspace & context. - public init(documentationBundleURL: URL, outputURL: URL, bundleIdentifier: String, diagnosticEngine: DiagnosticEngine = .init()) throws { + public init(archiveURL: URL, outputURL: URL, bundleIdentifier: String, diagnosticEngine: DiagnosticEngine = .init()) { // Initialize the action context. - self.rootURL = documentationBundleURL + self.archiveURL = archiveURL self.outputURL = outputURL self.bundleIdentifier = bundleIdentifier self.diagnosticEngine = diagnosticEngine - self.diagnosticEngine.add(DiagnosticConsoleWriter(formattingOptions: [], baseURL: documentationBundleURL)) + self.diagnosticEngine.add(DiagnosticConsoleWriter(formattingOptions: [], baseURL: archiveURL)) } /// Converts each eligible file from the source documentation bundle, @@ -40,8 +40,7 @@ public struct IndexAction: AsyncAction { } private func buildIndex() throws -> [Problem] { - let dataProvider = try LocalFileSystemDataProvider(rootURL: rootURL) - let indexBuilder = NavigatorIndex.Builder(renderNodeProvider: FileSystemRenderNodeProvider(fileSystemProvider: dataProvider), + let indexBuilder = NavigatorIndex.Builder(archiveURL: archiveURL, outputURL: outputURL, bundleIdentifier: bundleIdentifier, sortRootChildrenByName: true, diff --git a/Sources/SwiftDocCUtilities/Action/Actions/TransformForStaticHostingAction.swift b/Sources/SwiftDocCUtilities/Action/Actions/TransformForStaticHostingAction.swift index 68628831fe..cb68f0cb56 100644 --- a/Sources/SwiftDocCUtilities/Action/Actions/TransformForStaticHostingAction.swift +++ b/Sources/SwiftDocCUtilities/Action/Actions/TransformForStaticHostingAction.swift @@ -92,8 +92,7 @@ struct TransformForStaticHostingAction: AsyncAction { ) // Create a StaticHostableTransformer targeted at the archive data folder - let dataProvider = try LocalFileSystemDataProvider(rootURL: rootURL.appendingPathComponent(NodeURLGenerator.Path.dataFolderName)) - let transformer = StaticHostableTransformer(dataProvider: dataProvider, fileManager: fileManager, outputURL: outputURL, indexHTMLData: indexHTMLData) + let transformer = StaticHostableTransformer(dataDirectory: rootURL.appendingPathComponent(NodeURLGenerator.Path.dataFolderName), fileManager: fileManager, outputURL: outputURL, indexHTMLData: indexHTMLData) try transformer.transform() } diff --git a/Sources/SwiftDocCUtilities/ArgumentParsing/ActionExtensions/IndexAction+CommandInitialization.swift b/Sources/SwiftDocCUtilities/ArgumentParsing/ActionExtensions/IndexAction+CommandInitialization.swift index 4d3ba70d3d..1cdbb8f6f3 100644 --- a/Sources/SwiftDocCUtilities/ArgumentParsing/ActionExtensions/IndexAction+CommandInitialization.swift +++ b/Sources/SwiftDocCUtilities/ArgumentParsing/ActionExtensions/IndexAction+CommandInitialization.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2024 Apple Inc. and the Swift project authors Licensed under Apache License v2.0 with Runtime Library Exception See https://swift.org/LICENSE.txt for license information @@ -12,10 +12,10 @@ import Foundation extension IndexAction { /// Initializes ``IndexAction`` from the options in the ``Index`` command. - init(fromIndexCommand index: Docc.Index) throws { + init(fromIndexCommand index: Docc.Index) { // Initialize the `IndexAction` from the options provided by the `Index` command - try self.init( - documentationBundleURL: index.documentationBundle.urlOrFallback, + self.init( + archiveURL: index.documentationArchive.urlOrFallback, outputURL: index.outputURL, bundleIdentifier: index.bundleIdentifier) } diff --git a/Sources/SwiftDocCUtilities/ArgumentParsing/Subcommands/Index.swift b/Sources/SwiftDocCUtilities/ArgumentParsing/Subcommands/Index.swift index 273486325d..bbad44b44f 100644 --- a/Sources/SwiftDocCUtilities/ArgumentParsing/Subcommands/Index.swift +++ b/Sources/SwiftDocCUtilities/ArgumentParsing/Subcommands/Index.swift @@ -21,7 +21,7 @@ extension Docc { /// The user-provided path to a `.doccarchive` documentation archive. @OptionGroup() - public var documentationBundle: DocCArchiveOption + public var documentationArchive: DocCArchiveOption /// The user-provided bundle name to use for the produced index. @Option(help: "The bundle name for the index.") @@ -33,11 +33,11 @@ extension Docc { /// The path to the directory that all build output should be placed in. public var outputURL: URL { - documentationBundle.urlOrFallback.appendingPathComponent("index", isDirectory: true) + documentationArchive.urlOrFallback.appendingPathComponent("index", isDirectory: true) } public func run() async throws { - let indexAction = try IndexAction(fromIndexCommand: self) + let indexAction = IndexAction(fromIndexCommand: self) try await indexAction.performAndHandleResult() } } diff --git a/Sources/SwiftDocCUtilities/Transformers/StaticHostableTransformer.swift b/Sources/SwiftDocCUtilities/Transformers/StaticHostableTransformer.swift index fcb6ac1de9..ed3a97e466 100644 --- a/Sources/SwiftDocCUtilities/Transformers/StaticHostableTransformer.swift +++ b/Sources/SwiftDocCUtilities/Transformers/StaticHostableTransformer.swift @@ -32,122 +32,50 @@ enum StaticHostableTransformerError: DescribedError { /// Navigates the contents of a FileSystemProvider pointing at the data folder of a `.doccarchive` to emit a static hostable website. struct StaticHostableTransformer { - - /// The internal `FileSystemProvider` reference. - /// This should be the data folder of an archive. - private let dataProvider: FileSystemProvider - - /// Where the output will be written. + /// The data directory to create static hostable files for. + private let dataDirectory: URL + /// The directory to write the static hostable files in. private let outputURL: URL - - /// The index.html file to be used. + /// The index.html contents to write for each static hostable file. private let indexHTMLData: Data - + /// The file manager used to create directories and files. private let fileManager: FileManagerProtocol - /// Initialise with a dataProvider to the source doccarchive. + /// Initialize with a dataProvider to the source doccarchive. /// - Parameters: - /// - dataProvider: Should point to the data folder in a docc archive. - /// - fileManager: The FileManager to use for file processes. - /// - outputURL: The folder where the output will be placed - /// - indexHTMLData: Data representing the index.html to be written in the transformed folder structure. - init(dataProvider: FileSystemProvider, fileManager: FileManagerProtocol, outputURL: URL, indexHTMLData: Data) { - self.dataProvider = dataProvider + /// - dataDirectory: The data directory to create static hostable files for. + /// - fileManager: The file manager used to create directories and files. + /// - outputURL: The output directory where the transformer will write the static hostable files in. + /// - indexHTMLData: Data representing the index.html content that the static + init(dataDirectory: URL, fileManager: FileManagerProtocol, outputURL: URL, indexHTMLData: Data) { + self.dataDirectory = dataDirectory.standardizedFileURL self.fileManager = fileManager - self.outputURL = outputURL + self.outputURL = outputURL.standardizedFileURL self.indexHTMLData = indexHTMLData } /// Creates a static hostable version of the documentation in the data folder of an archive pointed to by the `dataProvider` func transform() throws { - - let node = dataProvider.fileSystem - - // We should be starting at the data folder of a .doccarchive. - switch node { - case .directory(let dir): - try transformDirectoryContents(directoryRoot: outputURL, relativeSubPath: "", directoryContents: dir.children) - case .file(let file): - throw StaticHostableTransformerError.dataProviderDoesNotReferenceValidInput(url: file.url) - } - } - - - /// Create a directory at the provided URL - /// - private func createDirectory(url: URL) throws { - if !fileManager.fileExists(atPath: url.path) { - try fileManager.createDirectory(at: url, withIntermediateDirectories: true, attributes: [:]) - } - } - - /// Transforms the contents of a given directory - /// - Parameters: - /// - root: The root output URL - /// - directory: The relative path (to the root) of the directory for which then content will processed. - /// - nodes: The directory contents - /// - Returns: An array of problems that may have occurred during processing - private func transformDirectoryContents(directoryRoot: URL, relativeSubPath: String, directoryContents: [FSNode]) throws { - - for node in directoryContents { - switch node { - case .directory(let dir): - try transformDirectory(directoryRoot: directoryRoot, currentDirectoryNode: dir, directorySubPath: relativeSubPath) - case .file(let file): - let outputURL = directoryRoot.appendingPathComponent(relativeSubPath) - try transformFile(file: file, outputURL: outputURL) + for file in fileManager.recursiveFiles(startingPoint: dataDirectory) where file.pathExtension.lowercased() == "json" { + // For each "/relative/something.json" file, create a "/relative/something/index.html" file. + + guard let relativeFileURL = file.relative(to: dataDirectory) else { + // Our `URL.relative(to:)` extension only return `nil` if the URLComponents aren't valid. + continue } - } - - } - - /// Transform the given directory - /// - Parameters: - /// - root: The root output URL - /// - dir: The FSNode that represents the directory - /// - currentDirectory: The relative path (to the root) of the directory that will contain this directory - private func transformDirectory(directoryRoot: URL, currentDirectoryNode: FSNode.Directory, directorySubPath: String) throws { - - // Create the path for the new directory - var newDirectory = directorySubPath - let newPathComponent = currentDirectoryNode.url.lastPathComponent - - // We need to ensure the new directory component, if not empty, ends with / - if !newDirectory.isEmpty && !newDirectory.hasSuffix("/") { - newDirectory += "/" - } - newDirectory += newPathComponent - - - // Create the HTML output directory - - let htmlOutputURL = directoryRoot.appendingPathComponent(newDirectory) - try createDirectory(url: htmlOutputURL) - - // Process the directory contents - try transformDirectoryContents(directoryRoot: directoryRoot, relativeSubPath: newDirectory, directoryContents: currentDirectoryNode.children) - - } - - /// Transform the given File - /// - Parameters: - /// - file: The FSNode that represents the file - /// - outputURL: The directory the need to be placed in - private func transformFile(file: FSNode.File, outputURL: URL) throws { - - // For JSON files we need to create an associated index.html in a sub-folder of the same name. - guard file.url.pathExtension.lowercased() == "json" else { return } - - let dirURL = file.url.deletingPathExtension() - let newDir = dirURL.lastPathComponent - let newDirURL = outputURL.appendingPathComponent(newDir) + + let outputDirectoryURL = outputURL.appendingPathComponent( + relativeFileURL.deletingPathExtension().path, // A directory with the same base name as the file + isDirectory: true + ) - if !fileManager.fileExists(atPath: newDirURL.path) { - try fileManager.createDirectory(at: newDirURL, withIntermediateDirectories: true, attributes: [:]) + // Ensure that the intermediate directories exist + if !fileManager.fileExists(atPath: outputDirectoryURL.path) { + try fileManager.createDirectory(at: outputDirectoryURL, withIntermediateDirectories: true, attributes: [:]) + } + + try fileManager.createFile(at: outputDirectoryURL.appendingPathComponent("index.html"), contents: indexHTMLData) } - - let fileURL = newDirURL.appendingPathComponent("index.html") - try self.indexHTMLData.write(to: fileURL) } } diff --git a/Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift b/Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift index 13adf2759f..d932b8d037 100644 --- a/Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift +++ b/Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift @@ -1946,8 +1946,8 @@ class ConvertActionTests: XCTestCase { // Run just the index command over the built documentation - let indexAction = try IndexAction( - documentationBundleURL: targetURL, + let indexAction = IndexAction( + archiveURL: targetURL, outputURL: indexURL, bundleIdentifier: indexFromConvertAction.bundleIdentifier ) diff --git a/Tests/SwiftDocCUtilitiesTests/IndexActionTests.swift b/Tests/SwiftDocCUtilitiesTests/IndexActionTests.swift index 982794eaea..d118aa6593 100644 --- a/Tests/SwiftDocCUtilitiesTests/IndexActionTests.swift +++ b/Tests/SwiftDocCUtilitiesTests/IndexActionTests.swift @@ -50,8 +50,8 @@ class IndexActionTests: XCTestCase { let engine = DiagnosticEngine(filterLevel: .warning) - let indexAction = try IndexAction( - documentationBundleURL: targetBundleURL, + let indexAction = IndexAction( + archiveURL: targetBundleURL, outputURL: indexURL, bundleIdentifier: bundleIdentifier, diagnosticEngine: engine @@ -61,6 +61,7 @@ class IndexActionTests: XCTestCase { let index = try NavigatorIndex.readNavigatorIndex(url: indexURL) resultIndexDumps.insert(index.navigatorTree.root.dumpTree()) + XCTAssert(engine.problems.isEmpty, "Unexpected problems:\n\(engine.problems.map(\.diagnostic.summary).joined(separator: "\n"))") XCTAssertTrue(engine.problems.isEmpty, "Indexing bundle at \(targetURL) resulted in unexpected issues") } @@ -91,8 +92,8 @@ class IndexActionTests: XCTestCase { let bundleIdentifier = "org.swift.docc.example" let indexURL = targetURL.appendingPathComponent("index") let engine = DiagnosticEngine(filterLevel: .warning) - let indexAction = try IndexAction( - documentationBundleURL: targetBundleURL, + let indexAction = IndexAction( + archiveURL: targetBundleURL, outputURL: indexURL, bundleIdentifier: bundleIdentifier, diagnosticEngine: engine diff --git a/Tests/SwiftDocCUtilitiesTests/StaticHostableTransformerTests.swift b/Tests/SwiftDocCUtilitiesTests/StaticHostableTransformerTests.swift index d348e37791..d32e70f6b3 100644 --- a/Tests/SwiftDocCUtilitiesTests/StaticHostableTransformerTests.swift +++ b/Tests/SwiftDocCUtilitiesTests/StaticHostableTransformerTests.swift @@ -55,8 +55,7 @@ class StaticHostableTransformerTests: StaticHostingBaseTests { let indexHTMLData = try StaticHostableTransformer.indexHTMLData(in: testTemplateURL, with: basePath, fileManager: fileManager) let dataURL = targetBundleURL.appendingPathComponent(NodeURLGenerator.Path.dataFolderName) - let dataProvider = try LocalFileSystemDataProvider(rootURL: dataURL) - let transformer = StaticHostableTransformer(dataProvider: dataProvider, fileManager: fileManager, outputURL: outputURL, indexHTMLData: indexHTMLData) + let transformer = StaticHostableTransformer(dataDirectory: dataURL, fileManager: fileManager, outputURL: outputURL, indexHTMLData: indexHTMLData) try transformer.transform() @@ -144,7 +143,6 @@ class StaticHostableTransformerTests: StaticHostingBaseTests { _ = try await action.perform(logHandle: .none) let dataURL = targetBundleURL.appendingPathComponent(NodeURLGenerator.Path.dataFolderName) - let dataProvider = try LocalFileSystemDataProvider(rootURL: dataURL) let testTemplateURL = try createTemporaryDirectory().appendingPathComponent("testTemplate") try Folder.testHTMLTemplateDirectory.write(to: testTemplateURL) @@ -160,10 +158,10 @@ class StaticHostableTransformerTests: StaticHostingBaseTests { let fileManager = FileManager.default for (basePath, testValue) in basePaths { - let outputURL = try createTemporaryDirectory().appendingPathComponent("output") + let outputURL = try createTemporaryDirectory().appendingPathComponent("output/") let indexHTMLData = try StaticHostableTransformer.indexHTMLData(in: testTemplateURL, with: basePath, fileManager: FileManager.default) - let transformer = StaticHostableTransformer(dataProvider: dataProvider, fileManager: fileManager, outputURL: outputURL, indexHTMLData: indexHTMLData) + let transformer = StaticHostableTransformer(dataDirectory: dataURL, fileManager: fileManager, outputURL: outputURL, indexHTMLData: indexHTMLData) try transformer.transform() diff --git a/Tests/SwiftDocCUtilitiesTests/Utility/TestFileSystemTests.swift b/Tests/SwiftDocCUtilitiesTests/Utility/TestFileSystemTests.swift index af4318698c..0108e846ef 100644 --- a/Tests/SwiftDocCUtilitiesTests/Utility/TestFileSystemTests.swift +++ b/Tests/SwiftDocCUtilitiesTests/Utility/TestFileSystemTests.swift @@ -133,6 +133,18 @@ class TestFileSystemTests: XCTestCase { │ ╰─ myfile2.txt ╰─ tmp/ """) + + let filesIterator = fs.recursiveFiles(startingPoint: URL(fileURLWithPath: "/")) + XCTAssertEqual(filesIterator.prefix(2).map(\.path).sorted(), [ + // Shallow files first + "/copy/myfile1.txt", + "/copy/myfile2.txt", + ]) + XCTAssertEqual(filesIterator.dropFirst(2).map(\.path).sorted(), [ + // Deeper files after + "/main/nested/myfile1.txt", + "/main/nested/myfile2.txt", + ]) } @@ -175,6 +187,10 @@ class TestFileSystemTests: XCTestCase { │ ╰─ myfile2.txt ╰─ tmp/ """) + + XCTAssertEqual(fs.recursiveFiles(startingPoint: URL(fileURLWithPath: "/")).map(\.lastPathComponent), [ + "myfile2.txt", + ]) } func testRemoveFolders() throws { @@ -239,6 +255,10 @@ class TestFileSystemTests: XCTestCase { │ ╰─ myfile2.txt ╰─ tmp/ """) + + XCTAssertEqual(fs.recursiveFiles(startingPoint: URL(fileURLWithPath: "/")).map(\.lastPathComponent).sorted(), [ + "myfile1.txt", "myfile2.txt", + ]) } func testCreateDeeplyNestedDirectory() throws { @@ -257,6 +277,8 @@ class TestFileSystemTests: XCTestCase { │ ╰─ six/ ╰─ tmp/ """) + + XCTAssertEqual(fs.recursiveFiles(startingPoint: URL(fileURLWithPath: "/")).map(\.lastPathComponent), [], "Only directories. No files.") } func testFileExists() throws {