-
Notifications
You must be signed in to change notification settings - Fork 182
Add Static Hosting Support #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
5d4a4ea
2bc5a86
8a5d6cd
dfe24b4
9b054eb
2d3b228
60120e4
6087d4f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,7 +38,11 @@ public struct ConvertAction: Action, RecreatingContext { | |
| let documentationCoverageOptions: DocumentationCoverageOptions | ||
| let diagnosticLevel: DiagnosticSeverity | ||
| let diagnosticEngine: DiagnosticEngine | ||
|
|
||
|
|
||
| let transformForStaticHosting: Bool | ||
| let staticHostingBasePath: String? | ||
|
|
||
|
|
||
| private(set) var context: DocumentationContext { | ||
| didSet { | ||
| // current platforms? | ||
|
|
@@ -88,7 +92,10 @@ public struct ConvertAction: Action, RecreatingContext { | |
| diagnosticEngine: DiagnosticEngine? = nil, | ||
| emitFixits: Bool = false, | ||
| inheritDocs: Bool = false, | ||
| experimentalEnableCustomTemplates: Bool = false) throws | ||
| experimentalEnableCustomTemplates: Bool = false, | ||
| transformForStaticHosting: Bool = false, | ||
| staticHostingBasePath: String? = nil | ||
| ) throws | ||
| { | ||
| self.rootURL = documentationBundleURL | ||
| self.outOfProcessResolver = outOfProcessResolver | ||
|
|
@@ -101,7 +108,9 @@ public struct ConvertAction: Action, RecreatingContext { | |
| self.injectedDataProvider = dataProvider | ||
| self.fileManager = fileManager | ||
| self.documentationCoverageOptions = documentationCoverageOptions | ||
|
|
||
| self.transformForStaticHosting = transformForStaticHosting | ||
| self.staticHostingBasePath = staticHostingBasePath | ||
|
|
||
| let filterLevel: DiagnosticSeverity | ||
| if analyze { | ||
| filterLevel = .information | ||
|
|
@@ -189,7 +198,9 @@ public struct ConvertAction: Action, RecreatingContext { | |
| diagnosticEngine: DiagnosticEngine? = nil, | ||
| emitFixits: Bool = false, | ||
| inheritDocs: Bool = false, | ||
| experimentalEnableCustomTemplates: Bool = false | ||
| experimentalEnableCustomTemplates: Bool = false, | ||
| transformForStaticHosting: Bool, | ||
| staticHostingBasePath: String? | ||
| ) throws { | ||
| // Note: This public initializer exists separately from the above internal one | ||
| // because the FileManagerProtocol type we use to enable mocking in tests | ||
|
|
@@ -217,7 +228,9 @@ public struct ConvertAction: Action, RecreatingContext { | |
| diagnosticEngine: diagnosticEngine, | ||
| emitFixits: emitFixits, | ||
| inheritDocs: inheritDocs, | ||
| experimentalEnableCustomTemplates: experimentalEnableCustomTemplates | ||
| experimentalEnableCustomTemplates: experimentalEnableCustomTemplates, | ||
| transformForStaticHosting: transformForStaticHosting, | ||
| staticHostingBasePath: staticHostingBasePath | ||
| ) | ||
| } | ||
|
|
||
|
|
@@ -240,7 +253,7 @@ public struct ConvertAction: Action, RecreatingContext { | |
| mutating func cancel() throws { | ||
| /// If the action is not running, there is nothing to cancel | ||
| guard isPerforming.sync({ $0 }) == true else { return } | ||
|
|
||
| /// If the action is already cancelled throw `cancelPending`. | ||
| if isCancelled.sync({ $0 }) == true { | ||
| throw Error.cancelPending | ||
|
|
@@ -277,6 +290,15 @@ public struct ConvertAction: Action, RecreatingContext { | |
|
|
||
| let temporaryFolder = try createTempFolder( | ||
| with: htmlTemplateDirectory) | ||
|
|
||
| // The `template-index.html` is a duplicate version of `index.html` with extra template | ||
| // tokens that allow for customizing the base-path used when transforming | ||
| // for a static hosting environment. We don't want to include it when copying over | ||
| // the base template. | ||
| let templateURL: URL = temporaryFolder.appendingPathComponent(HTMLTemplate.templateFileName.rawValue) | ||
| if fileManager.fileExists(atPath: templateURL.path) { | ||
| try fileManager.removeItem(at: templateURL) | ||
| } | ||
|
|
||
| defer { | ||
| try? fileManager.removeItem(at: temporaryFolder) | ||
|
|
@@ -330,13 +352,20 @@ public struct ConvertAction: Action, RecreatingContext { | |
| allProblems.append(contentsOf: indexerProblems) | ||
| } | ||
|
|
||
| // Process Static Hosting is needed. | ||
| if transformForStaticHosting, let templateDirectory = htmlTemplateDirectory { | ||
| let dataProvider = try LocalFileSystemDataProvider(rootURL: temporaryFolder.appendingPathComponent("data")) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're now constructing the data directory by hand in several different places ( |
||
| let transformer = try StaticHostableTransformer(dataProvider: dataProvider, fileManager: fileManager, outputURL: temporaryFolder, htmlTemplate: templateDirectory, staticHostingBasePath: staticHostingBasePath) | ||
| try transformer.transform() | ||
| } | ||
|
|
||
| // We should generally only replace the current build output if we didn't encounter errors | ||
| // during conversion. However, if the `emitDigest` flag is true, | ||
| // we should replace the current output with our digest of problems. | ||
| if !allProblems.containsErrors || emitDigest { | ||
| try moveOutput(from: temporaryFolder, to: targetDirectory) | ||
| } | ||
|
|
||
| // Log the output size. | ||
| benchmark(add: Benchmark.OutputSize(dataURL: targetDirectory.appendingPathComponent("data"))) | ||
|
|
||
|
|
@@ -363,6 +392,7 @@ public struct ConvertAction: Action, RecreatingContext { | |
| } | ||
|
|
||
| func createTempFolder(with templateURL: URL?) throws -> URL { | ||
|
|
||
| let targetURL = URL(fileURLWithPath: NSTemporaryDirectory()) | ||
| .appendingPathComponent(ProcessInfo.processInfo.globallyUniqueString) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,110 @@ | ||||||
| /* | ||||||
| This source file is part of the Swift.org open source project | ||||||
|
|
||||||
| Copyright (c) 2021 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 | ||||||
| import SwiftDocC | ||||||
|
|
||||||
| /// An action that emits a static hostable website from a DocC Archive. | ||||||
| struct TransformForStaticHostingAction: Action { | ||||||
|
|
||||||
| let rootURL: URL | ||||||
| let outputURL: URL | ||||||
| let staticHostingBasePath: String? | ||||||
| let outputIsExternal: Bool | ||||||
| let htmlTemplateDirectory: URL | ||||||
|
|
||||||
| let fileManager: FileManagerProtocol | ||||||
|
|
||||||
| var diagnosticEngine: DiagnosticEngine | ||||||
|
|
||||||
| /// Initializes the action with the given validated options, creates or uses the given action workspace & context. | ||||||
| init(documentationBundleURL: URL, | ||||||
| outputURL:URL?, | ||||||
| staticHostingBasePath: String?, | ||||||
| htmlTemplateDirectory: URL, | ||||||
| fileManager: FileManagerProtocol = FileManager.default, | ||||||
| diagnosticEngine: DiagnosticEngine = .init()) throws | ||||||
| { | ||||||
| // Initialize the action context. | ||||||
| self.rootURL = documentationBundleURL | ||||||
| self.outputURL = outputURL ?? documentationBundleURL | ||||||
| self.outputIsExternal = outputURL != nil | ||||||
| self.staticHostingBasePath = staticHostingBasePath | ||||||
| self.htmlTemplateDirectory = htmlTemplateDirectory | ||||||
| self.fileManager = fileManager | ||||||
| self.diagnosticEngine = diagnosticEngine | ||||||
| self.diagnosticEngine.add(DiagnosticConsoleWriter(formattingOptions: [])) | ||||||
| } | ||||||
|
|
||||||
| /// Converts each eligable file from the source archive, | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| /// saves the results in the given output folder. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| mutating func perform(logHandle: LogHandle) throws -> ActionResult { | ||||||
| try emit() | ||||||
| return ActionResult(didEncounterError: false, outputs: [outputURL]) | ||||||
| } | ||||||
|
|
||||||
| mutating private func emit() throws { | ||||||
|
|
||||||
|
|
||||||
| // If the emit is to create the static hostable content outside of the source archive | ||||||
| // then the output folder needs to be set up and the archive data copied | ||||||
| // to the new folder. | ||||||
| if outputIsExternal { | ||||||
|
|
||||||
| try setupOutputDirectory(outputURL: outputURL) | ||||||
|
|
||||||
| // Copy the appropriate folders from the archive. | ||||||
| // We will do it item as we want to preserve anything intentionally left in the output URL by `setupOutputDirectory` | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure I follow this second sentence, especially the "We will do it item as we want" part. |
||||||
| for sourceItem in try fileManager.contentsOfDirectory(at: rootURL, includingPropertiesForKeys: [], options:[.skipsHiddenFiles]) { | ||||||
| let targetItem = outputURL.appendingPathComponent(sourceItem.lastPathComponent) | ||||||
| try fileManager.copyItem(at: sourceItem, to: targetItem) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // Copy the HTML template to the output folder. | ||||||
| var excludedFiles = [HTMLTemplate.templateFileName.rawValue] | ||||||
|
|
||||||
| if outputIsExternal { | ||||||
| excludedFiles.append(HTMLTemplate.indexFileName.rawValue) | ||||||
| } | ||||||
|
|
||||||
| for content in try fileManager.contentsOfDirectory(atPath: htmlTemplateDirectory.path) { | ||||||
|
|
||||||
| guard !excludedFiles.contains(content) else { continue } | ||||||
|
|
||||||
| let source = htmlTemplateDirectory.appendingPathComponent(content) | ||||||
| let target = outputURL.appendingPathComponent(content) | ||||||
| if fileManager.fileExists(atPath: target.path){ | ||||||
| try fileManager.removeItem(at: target) | ||||||
| } | ||||||
| try fileManager.copyItem(at: source, to: target) | ||||||
| } | ||||||
|
|
||||||
| // Create a StaticHostableTransformer targeted at the archive data folder | ||||||
| let dataProvider = try LocalFileSystemDataProvider(rootURL: rootURL.appendingPathComponent("data")) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another place where the data directory is constructed. |
||||||
| let transformer = try StaticHostableTransformer(dataProvider: dataProvider, fileManager: fileManager, outputURL: outputURL, htmlTemplate: htmlTemplateDirectory, staticHostingBasePath: staticHostingBasePath) | ||||||
| try transformer.transform() | ||||||
|
|
||||||
| } | ||||||
|
|
||||||
| /// Create ouput directory or empty its contents if it already exists. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| private func setupOutputDirectory(outputURL: URL) throws { | ||||||
|
|
||||||
| var isDirectory: ObjCBool = false | ||||||
| if fileManager.fileExists(atPath: outputURL.path, isDirectory: &isDirectory), isDirectory.boolValue { | ||||||
| let contents = try fileManager.contentsOfDirectory(at: outputURL, includingPropertiesForKeys: [], options: [.skipsHiddenFiles]) | ||||||
| for content in contents { | ||||||
| try fileManager.removeItem(at: content) | ||||||
| } | ||||||
| } else { | ||||||
| try fileManager.createDirectory(at: outputURL, withIntermediateDirectories: false, attributes: [:]) | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| /* | ||
| This source file is part of the Swift.org open source project | ||
|
|
||
| Copyright (c) 2021 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 | ||
| import ArgumentParser | ||
|
|
||
|
|
||
| extension TransformForStaticHostingAction { | ||
| /// Initializes ``TransformForStaticHostingAction`` from the options in the ``TransformForStaticHosting`` command. | ||
| /// - Parameters: | ||
| /// - cmd: The emit command this `TransformForStaticHostingAction` will be based on. | ||
| init(fromCommand cmd: Docc.ProcessArchive.TransformForStaticHosting, withFallbackTemplate fallbackTemplateURL: URL? = nil) throws { | ||
| // Initialize the `TransformForStaticHostingAction` from the options provided by the `EmitStaticHostable` command | ||
|
|
||
| guard let htmlTemplateFolder = cmd.templateOption.templateURL ?? fallbackTemplateURL else { | ||
| throw ValidationError("No valid html Template folder has been provided") | ||
| } | ||
|
|
||
| try self.init( | ||
| documentationBundleURL: cmd.documentationArchive.urlOrFallback, | ||
| outputURL: cmd.outputURL, | ||
| staticHostingBasePath: cmd.staticHostingBasePath, | ||
| htmlTemplateDirectory: htmlTemplateFolder ) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -12,21 +12,40 @@ import ArgumentParser | |||||
| import Foundation | ||||||
|
|
||||||
| /// Resolves and validates a URL value that provides the path to a documentation archive. | ||||||
| /// | ||||||
| /// This option is used by the ``Docc/Index`` subcommand. | ||||||
| public struct DocumentationArchiveOption: DirectoryPathOption { | ||||||
| public struct DocCArchiveOption: DirectoryPathOption { | ||||||
|
|
||||||
| public init() {} | ||||||
| public init(){} | ||||||
|
|
||||||
| /// The name of the command line argument used to specify a source archive path. | ||||||
| /// The name of the command line argument used to specify a source bundle path. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure I follow the change here. the argument name is still |
||||||
| static let argumentValueName = "source-archive-path" | ||||||
| static let expectedContent: Set<String> = ["data"] | ||||||
|
|
||||||
| /// The path to an archive to be indexed by DocC. | ||||||
| /// The path to a archive to be used by DocC. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "an" was correct here.
Suggested change
|
||||||
| @Argument( | ||||||
| help: ArgumentHelp( | ||||||
| "Path to a documentation archive data directory of JSON files.", | ||||||
| discussion: "The '.doccarchive' bundle docc will index.", | ||||||
| "Path to the DocC Archive ('.doccarchive') that should be processed.", | ||||||
| valueName: argumentValueName), | ||||||
| transform: URL.init(fileURLWithPath:)) | ||||||
| public var url: URL? | ||||||
|
|
||||||
| public mutating func validate() throws { | ||||||
|
|
||||||
| // Validate that the URL represents a directory | ||||||
| guard urlOrFallback.hasDirectoryPath == true else { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This check seems a bit strict. It's not actually checking whether the file on disk is a directory, it's just checking whether the path string ends in a |
||||||
| throw ValidationError("'\(urlOrFallback.path)' is not a valid DocC Archive.") | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Explaining exactly why something isn't a valid docc archive (expected a directory but a path to a file was provided) can help make error messages less frustrating. |
||||||
| } | ||||||
|
|
||||||
| var archiveContents: [String] | ||||||
| do { | ||||||
| archiveContents = try FileManager.default.contentsOfDirectory(atPath: urlOrFallback.path) | ||||||
| } catch { | ||||||
| throw ValidationError("'\(urlOrFallback.path)' is not a valid DocC Archive: \(error)") | ||||||
| } | ||||||
|
|
||||||
| guard DocCArchiveOption.expectedContent.isSubset(of: Set(archiveContents)) else { | ||||||
| let missing = Array(Set(DocCArchiveOption.expectedContent).subtracting(archiveContents)) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of essentially performing the subtracting operation twice (isSubset and then also subtracting), you could just subtract and then the guard could check if the result is empty, and if not print it. |
||||||
| throw ValidationError("'\(urlOrFallback.path)' is not a valid DocC Archive. Missing: \(missing)") | ||||||
| } | ||||||
|
|
||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -155,7 +155,7 @@ extension Docc { | |||||
| help: "A fallback default language for code listings if no value is provided in the documentation bundle's Info.plist file." | ||||||
| ) | ||||||
| public var defaultCodeListingLanguage: String? | ||||||
|
|
||||||
| @Option( | ||||||
| help: """ | ||||||
| A fallback default module kind if no value is provided \ | ||||||
|
|
@@ -217,6 +217,20 @@ extension Docc { | |||||
|
|
||||||
| return outputURL | ||||||
| } | ||||||
|
|
||||||
| /// Defaults to false | ||||||
| @Flag(help: "Produce a Swift-DocC Archive that supports a static hosting environment.") | ||||||
| public var transformForStaticHosting = false | ||||||
|
|
||||||
| /// A user-provided relative path to be used in the archived output | ||||||
| @Option( | ||||||
| name: [.customLong("static-hosting-base-path")], | ||||||
| help: ArgumentHelp( | ||||||
| "The base path your documentation website will be hosted at.", | ||||||
| discussion: "For example, to deploy your site to 'example.com/my_name/my_project/documentation' instead of 'example.com/documentation', pass '/my_name/my_project' as the base path.") | ||||||
| ) | ||||||
| var staticHostingBasePath: String? | ||||||
|
|
||||||
|
|
||||||
| // MARK: - Property Validation | ||||||
|
|
||||||
|
|
@@ -234,6 +248,31 @@ extension Docc { | |||||
| throw ValidationError("No directory exist at '\(outputParent.path)'.") | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| if transformForStaticHosting { | ||||||
| if let templateURL = templateOption.templateURL { | ||||||
| let neededFileName: String | ||||||
|
|
||||||
| if staticHostingBasePath != nil { | ||||||
| neededFileName = HTMLTemplate.templateFileName.rawValue | ||||||
| }else { | ||||||
| neededFileName = HTMLTemplate.indexFileName.rawValue | ||||||
| } | ||||||
|
|
||||||
| let indexTemplate = templateURL.appendingPathComponent(neededFileName) | ||||||
| if !FileManager.default.fileExists(atPath: indexTemplate.path) { | ||||||
| throw ValidationError("You cannot Transform for Static Hosting as the provided template (\(TemplateOption.environmentVariableKey)) does not contain a valid \(neededFileName) file.") | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| } | ||||||
|
|
||||||
| } else { | ||||||
| throw ValidationError( | ||||||
| """ | ||||||
| Invalid or missing HTML template directory, relative to the docc executable, at: \(templateOption.defaultTemplateURL.path) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| Set the '\(TemplateOption.environmentVariableKey)' environment variable to use a custom HTML template. | ||||||
| """) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| } | ||||||
|
|
||||||
| // MARK: - Execution | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.