Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -217,7 +228,9 @@ public struct ConvertAction: Action, RecreatingContext {
diagnosticEngine: diagnosticEngine,
emitFixits: emitFixits,
inheritDocs: inheritDocs,
experimentalEnableCustomTemplates: experimentalEnableCustomTemplates
experimentalEnableCustomTemplates: experimentalEnableCustomTemplates,
transformForStaticHosting: transformForStaticHosting,
staticHostingBasePath: staticHostingBasePath
)
}

Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -330,13 +352,20 @@ public struct ConvertAction: Action, RecreatingContext {
allProblems.append(contentsOf: indexerProblems)
}

// Process Static Hosting is needed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Process Static Hosting is needed.
// Process Static Hosting as needed.

if transformForStaticHosting, let templateDirectory = htmlTemplateDirectory {
let dataProvider = try LocalFileSystemDataProvider(rootURL: temporaryFolder.appendingPathComponent("data"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 (temporaryFolder.appendingPathComponent("data")), it'd be nice if there was a constant or computed property or something more centralized so this string and path manipulation wasn't replicated.

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")))

Expand All @@ -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)

Expand Down
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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Converts each eligable file from the source archive,
/// Converts each eligible file from the source archive and

/// saves the results in the given output folder.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// saves the results in the given output folder.
/// saves the results in the given output folder.

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`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Create ouput directory or empty its contents if it already exists.
/// Create output directory or empty its contents if it already exists.

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
Expand Up @@ -79,7 +79,9 @@ extension ConvertAction {
diagnosticLevel: convert.diagnosticLevel,
emitFixits: convert.emitFixits,
inheritDocs: convert.enableInheritedDocs,
experimentalEnableCustomTemplates: convert.experimentalEnableCustomTemplates
experimentalEnableCustomTemplates: convert.experimentalEnableCustomTemplates,
transformForStaticHosting: convert.transformForStaticHosting,
staticHostingBasePath: convert.staticHostingBasePath
)
}
}
Expand Down
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
Expand Up @@ -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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I follow the change here. the argument name is still source-archive-path but the comment was changed from source archive path to source bundle path. Is this the path to a docc catalog (in which case the argument value name and comment should both change to `catalog, or is this a doccarchive? If it's a doccarchive, I'm not sure why this comment was changed.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"an" was correct here.

Suggested change
/// The path to a archive to be used by DocC.
/// The path to an archive to be used by DocC.

@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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
guard urlOrFallback.hasDirectoryPath == true else {
guard urlOrFallback.hasDirectoryPath else {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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
Expand Up @@ -65,15 +65,16 @@ public struct TemplateOption: ParsableArguments {

// Only perform further validation if a templateURL has been provided
guard let templateURL = templateURL else {
if FileManager.default.fileExists(atPath: defaultTemplateURL.appendingPathComponent("index.html").path) {
if FileManager.default.fileExists(atPath: defaultTemplateURL.appendingPathComponent(HTMLTemplate.indexFileName.rawValue).path) {
self.templateURL = defaultTemplateURL
}
return
}

// Confirm that the provided directory contains an 'index.html' file which is a required part of
// an HTML template for docc.
guard FileManager.default.fileExists(atPath: templateURL.appendingPathComponent("index.html").path) else {
guard FileManager.default.fileExists(atPath: templateURL.appendingPathComponent(HTMLTemplate.indexFileName.rawValue).path)
else {
throw ValidationError(
"""
Invalid HTML template directory configuration provided via the '\(TemplateOption.environmentVariableKey)' environment variable.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down Expand Up @@ -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

Expand All @@ -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.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throw ValidationError("You cannot Transform for Static Hosting as the provided template (\(TemplateOption.environmentVariableKey)) does not contain a valid \(neededFileName) file.")
throw ValidationError("You cannot Transform for Static Hosting as the provided template (\(TemplateOption.environmentVariableKey)) does not contain a valid \(neededFileName) file.")

}

} else {
throw ValidationError(
"""
Invalid or missing HTML template directory, relative to the docc executable, at: \(templateOption.defaultTemplateURL.path)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Invalid or missing HTML template directory, relative to the docc executable, at: \(templateOption.defaultTemplateURL.path)
Invalid or missing HTML template directory, relative to the docc executable, at: \(templateOption.defaultTemplateURL.path).

Set the '\(TemplateOption.environmentVariableKey)' environment variable to use a custom HTML template.
""")
}
}

}

// MARK: - Execution
Expand Down
Loading