Skip to content

Commit 9dccbd8

Browse files
authored
Add mermaid charts support to swift package describe (#7289)
This adds support for new `--type mermaid` option on `swift package describe`. This format is supported by the GitHub Markdown renderer and some Markdown preview extensions in IDEs. I'm using different shapes to denote difference between products and targets. `| Foo |` is a target, `|| Bar ||` is a product, and hexagonal shapes declared with `{{ Baz }}` are products from package dependencies. I initially tried to include a legend subgraph, but that makes layout quite ugly even for simple packages, so I decided to exclude that by default. The codepath for rendering the legend is still there, so we can reenable it at any point or make conditional on some new CLI flag if needed.
1 parent ca2fe64 commit 9dccbd8

File tree

8 files changed

+615
-236
lines changed

8 files changed

+615
-236
lines changed

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ let package = Package(
387387
name: "Commands",
388388
dependencies: [
389389
.product(name: "ArgumentParser", package: "swift-argument-parser"),
390+
.product(name: "OrderedCollections", package: "swift-collections"),
390391
"Basics",
391392
"Build",
392393
"CoreCommands",

Sources/Commands/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,15 @@ add_library(Commands
4141
Utilities/DependenciesSerializer.swift
4242
Utilities/DescribedPackage.swift
4343
Utilities/DOTManifestSerializer.swift
44+
Utilities/MermaidPackageSerializer.swift
4445
Utilities/MultiRootSupport.swift
46+
Utilities/PlainTextEncoder.swift
4547
Utilities/PluginDelegate.swift
4648
Utilities/SymbolGraphExtract.swift
4749
Utilities/TestingSupport.swift
4850
Utilities/XCTEvents.swift)
4951
target_link_libraries(Commands PUBLIC
52+
SwiftCollections::OrderedCollections
5053
ArgumentParser
5154
Basics
5255
Build

Sources/Commands/PackageTools/Describe.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ extension SwiftPackageTool {
2626
@OptionGroup(visibility: .hidden)
2727
var globalOptions: GlobalOptions
2828

29-
@Option(help: "json | text")
29+
@Option(help: "json | text | mermaid")
3030
var type: DescribeMode = .text
3131

3232
func run(_ swiftTool: SwiftTool) async throws {
@@ -57,6 +57,8 @@ extension SwiftPackageTool {
5757
var encoder = PlainTextEncoder()
5858
encoder.formattingOptions = [.prettyPrinted]
5959
data = try encoder.encode(desc)
60+
case .mermaid:
61+
data = Data(MermaidPackageSerializer(package: package).renderedMarkdown.utf8)
6062
}
6163
print(String(decoding: data, as: UTF8.self))
6264
}
@@ -66,6 +68,8 @@ extension SwiftPackageTool {
6668
case json
6769
/// Human readable format (not guaranteed to be parsable).
6870
case text
71+
/// Mermaid flow charts format
72+
case mermaid
6973
}
7074
}
7175
}

Sources/Commands/Utilities/DOTManifestSerializer.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ import LLBuildManifest
1515
import protocol TSCBasic.OutputByteStream
1616

1717
/// Serializes an LLBuildManifest graph to a .dot file
18-
public struct DOTManifestSerializer {
18+
struct DOTManifestSerializer {
1919
var kindCounter = [String: Int]()
2020
var hasEmittedStyling = Set<String>()
2121
let manifest: LLBuildManifest
2222

2323
/// Creates a serializer that will serialize the given manifest.
24-
public init(manifest: LLBuildManifest) {
24+
init(manifest: LLBuildManifest) {
2525
self.manifest = manifest
2626
}
2727

@@ -38,11 +38,11 @@ public struct DOTManifestSerializer {
3838

3939
/// Quote the name and escape the quotes and backslashes
4040
func quoteName(_ name: String) -> String {
41-
return "\"" + name.replacingOccurrences(of: "\"", with: "\\\"")
42-
.replacingOccurrences(of: "\\", with: "\\\\") + "\""
41+
"\"" + name.replacingOccurrences(of: "\"", with: "\\\"")
42+
.replacingOccurrences(of: "\\", with: "\\\\") + "\""
4343
}
4444

45-
public mutating func writeDOT(to stream: OutputByteStream) {
45+
mutating func writeDOT(to stream: OutputByteStream) {
4646
stream.write("digraph Jobs {\n")
4747
for (name, command) in manifest.commands {
4848
let jobName = quoteName(label(for: command))

Sources/Commands/Utilities/DescribedPackage.swift

Lines changed: 1 addition & 230 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ struct DescribedPackage: Encodable {
114114
case registry
115115
}
116116

117-
public func encode(to encoder: Encoder) throws {
117+
func encode(to encoder: Encoder) throws {
118118
var container = encoder.container(keyedBy: CodingKeys.self)
119119
switch self {
120120
case .fileSystem(let identity, let path):
@@ -258,232 +258,3 @@ struct DescribedPackage: Encodable {
258258
}
259259
}
260260
}
261-
262-
263-
public struct PlainTextEncoder {
264-
265-
/// The formatting of the output plain-text data.
266-
public struct FormattingOptions: OptionSet {
267-
public let rawValue: UInt
268-
269-
public init(rawValue: UInt) {
270-
self.rawValue = rawValue
271-
}
272-
273-
/// Produce plain-text format with indented output.
274-
public static let prettyPrinted = FormattingOptions(rawValue: 1 << 0)
275-
}
276-
277-
/// The output format to produce. Defaults to `[]`.
278-
var formattingOptions: FormattingOptions = []
279-
280-
/// Contextual user-provided information for use during encoding.
281-
var userInfo: [CodingUserInfoKey: Any] = [:]
282-
283-
/// Encodes the given top-level value and returns its plain text representation.
284-
///
285-
/// - parameter value: The value to encode.
286-
/// - returns: A new `Data` value containing the encoded plan-text data.
287-
/// - throws: An error if any value throws an error during encoding.
288-
func encode<T: Encodable>(_ value: T) throws -> Data {
289-
let outputStream = BufferedOutputByteStream()
290-
let encoder = _PlainTextEncoder(outputStream: outputStream, formattingOptions: formattingOptions, userInfo: userInfo)
291-
try value.encode(to: encoder)
292-
return Data(outputStream.bytes.contents)
293-
}
294-
295-
/// Private helper function to format key names with an uppercase initial letter and space-separated components.
296-
private static func displayName(for key: CodingKey) -> String {
297-
var result = ""
298-
for ch in key.stringValue {
299-
if result.isEmpty {
300-
result.append(ch.uppercased())
301-
}
302-
else if ch.isUppercase {
303-
result.append(" ")
304-
result.append(ch.lowercased())
305-
}
306-
else {
307-
result.append(ch)
308-
}
309-
}
310-
return result
311-
}
312-
313-
/// Private Encoder implementation for PlainTextEncoder.
314-
private struct _PlainTextEncoder: Encoder {
315-
/// Output stream.
316-
var outputStream: OutputByteStream
317-
318-
/// Formatting options set on the top-level encoder.
319-
let formattingOptions: PlainTextEncoder.FormattingOptions
320-
321-
/// Contextual user-provided information for use during encoding.
322-
let userInfo: [CodingUserInfoKey: Any]
323-
324-
/// The path to the current point in encoding.
325-
let codingPath: [CodingKey]
326-
327-
/// Initializes `self` with the given top-level encoder options.
328-
init(outputStream: OutputByteStream, formattingOptions: PlainTextEncoder.FormattingOptions, userInfo: [CodingUserInfoKey: Any], codingPath: [CodingKey] = []) {
329-
self.outputStream = outputStream
330-
self.formattingOptions = formattingOptions
331-
self.userInfo = userInfo
332-
self.codingPath = codingPath
333-
}
334-
335-
func container<Key: CodingKey>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> {
336-
return KeyedEncodingContainer(PlainTextKeyedEncodingContainer<Key>(outputStream: outputStream, formattingOptions: formattingOptions, userInfo: userInfo, codingPath: codingPath))
337-
}
338-
339-
func unkeyedContainer() -> UnkeyedEncodingContainer {
340-
return PlainTextUnkeyedEncodingContainer(outputStream: outputStream, formattingOptions: formattingOptions, userInfo: userInfo, codingPath: codingPath)
341-
}
342-
343-
func singleValueContainer() -> SingleValueEncodingContainer {
344-
return TextSingleValueEncodingContainer(outputStream: outputStream, formattingOptions: formattingOptions, userInfo: userInfo, codingPath: codingPath)
345-
}
346-
347-
/// Private KeyedEncodingContainer implementation for PlainTextEncoder.
348-
private struct PlainTextKeyedEncodingContainer<Key: CodingKey>: KeyedEncodingContainerProtocol {
349-
let outputStream: OutputByteStream
350-
let formattingOptions: PlainTextEncoder.FormattingOptions
351-
let userInfo: [CodingUserInfoKey: Any]
352-
let codingPath: [CodingKey]
353-
354-
private mutating func emit(_ key: CodingKey, _ value: String?) {
355-
outputStream.send("\(String(repeating: " ", count: codingPath.count))\(displayName(for: key)):")
356-
if let value { outputStream.send(" \(value)") }
357-
outputStream.send("\n")
358-
}
359-
mutating func encodeNil(forKey key: Key) throws { emit(key, "nil") }
360-
mutating func encode(_ value: Bool, forKey key: Key) throws { emit(key, "\(value)") }
361-
mutating func encode(_ value: String, forKey key: Key) throws { emit(key, "\(value)") }
362-
mutating func encode(_ value: Double, forKey key: Key) throws { emit(key, "\(value)") }
363-
mutating func encode(_ value: Float, forKey key: Key) throws { emit(key, "\(value)") }
364-
mutating func encode(_ value: Int, forKey key: Key) throws { emit(key, "\(value)") }
365-
mutating func encode(_ value: Int8, forKey key: Key) throws { emit(key, "\(value)") }
366-
mutating func encode(_ value: Int16, forKey key: Key) throws { emit(key, "\(value)") }
367-
mutating func encode(_ value: Int32, forKey key: Key) throws { emit(key, "\(value)") }
368-
mutating func encode(_ value: Int64, forKey key: Key) throws { emit(key, "\(value)") }
369-
mutating func encode(_ value: UInt, forKey key: Key) throws { emit(key, "\(value)") }
370-
mutating func encode(_ value: UInt8, forKey key: Key) throws { emit(key, "\(value)") }
371-
mutating func encode(_ value: UInt16, forKey key: Key) throws { emit(key, "\(value)") }
372-
mutating func encode(_ value: UInt32, forKey key: Key) throws { emit(key, "\(value)") }
373-
mutating func encode(_ value: UInt64, forKey key: Key) throws { emit(key, "\(value)") }
374-
mutating func encode<T: Encodable>(_ value: T, forKey key: Key) throws {
375-
emit(key, nil)
376-
let textEncoder = _PlainTextEncoder(outputStream: outputStream, formattingOptions: formattingOptions, userInfo: userInfo, codingPath: codingPath + [key])
377-
try value.encode(to: textEncoder)
378-
}
379-
380-
mutating func nestedContainer<NestedKey: CodingKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> {
381-
emit(key, nil)
382-
return KeyedEncodingContainer(PlainTextKeyedEncodingContainer<NestedKey>(outputStream: outputStream, formattingOptions: formattingOptions, userInfo: userInfo, codingPath: codingPath + [key]))
383-
}
384-
385-
mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
386-
emit(key, nil)
387-
return PlainTextUnkeyedEncodingContainer(outputStream: outputStream, formattingOptions: formattingOptions, userInfo: userInfo, codingPath: codingPath + [key])
388-
}
389-
390-
mutating func superEncoder() -> Encoder {
391-
return superEncoder(forKey: Key(stringValue: "super")!)
392-
}
393-
394-
mutating func superEncoder(forKey key: Key) -> Encoder {
395-
return _PlainTextEncoder(outputStream: outputStream, formattingOptions: formattingOptions, userInfo: userInfo, codingPath: codingPath + [key])
396-
}
397-
}
398-
399-
/// Private UnkeyedEncodingContainer implementation for PlainTextEncoder.
400-
private struct PlainTextUnkeyedEncodingContainer: UnkeyedEncodingContainer {
401-
let outputStream: OutputByteStream
402-
let formattingOptions: PlainTextEncoder.FormattingOptions
403-
let userInfo: [CodingUserInfoKey: Any]
404-
let codingPath: [CodingKey]
405-
private(set) var count: Int = 0
406-
407-
private mutating func emit(_ value: String) {
408-
outputStream.send("\(String(repeating: " ", count: codingPath.count))\(value)\n")
409-
count += 1
410-
}
411-
mutating func encodeNil() throws { emit("nil") }
412-
mutating func encode(_ value: Bool) throws { emit("\(value)") }
413-
mutating func encode(_ value: String) throws { emit("\(value)") }
414-
mutating func encode(_ value: Double) throws { emit("\(value)") }
415-
mutating func encode(_ value: Float) throws { emit("\(value)") }
416-
mutating func encode(_ value: Int) throws { emit("\(value)") }
417-
mutating func encode(_ value: Int8) throws { emit("\(value)") }
418-
mutating func encode(_ value: Int16) throws { emit("\(value)") }
419-
mutating func encode(_ value: Int32) throws { emit("\(value)") }
420-
mutating func encode(_ value: Int64) throws { emit("\(value)") }
421-
mutating func encode(_ value: UInt) throws { emit("\(value)") }
422-
mutating func encode(_ value: UInt8) throws { emit("\(value)") }
423-
mutating func encode(_ value: UInt16) throws { emit("\(value)") }
424-
mutating func encode(_ value: UInt32) throws { emit("\(value)") }
425-
mutating func encode(_ value: UInt64) throws { emit("\(value)") }
426-
mutating func encode<T: Encodable>(_ value: T) throws {
427-
let textEncoder = _PlainTextEncoder(
428-
outputStream: outputStream,
429-
formattingOptions: formattingOptions,
430-
userInfo: userInfo,
431-
codingPath: codingPath
432-
)
433-
try value.encode(to: textEncoder)
434-
count += 1
435-
// FIXME: This is a bit arbitrary and should be controllable. We may also want an option to only emit
436-
// newlines between entries, not after each one.
437-
if codingPath.count < 2 { outputStream.send("\n") }
438-
}
439-
440-
mutating func nestedContainer<NestedKey: CodingKey>(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> {
441-
KeyedEncodingContainer(PlainTextKeyedEncodingContainer<NestedKey>(
442-
outputStream: outputStream,
443-
formattingOptions: formattingOptions,
444-
userInfo: userInfo,
445-
codingPath: codingPath
446-
))
447-
}
448-
449-
mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
450-
return PlainTextUnkeyedEncodingContainer(outputStream: outputStream, formattingOptions: formattingOptions, userInfo: userInfo, codingPath: codingPath)
451-
}
452-
453-
mutating func superEncoder() -> Encoder {
454-
return _PlainTextEncoder(outputStream: outputStream, formattingOptions: formattingOptions, userInfo: userInfo, codingPath: codingPath)
455-
}
456-
}
457-
458-
/// Private SingleValueEncodingContainer implementation for PlainTextEncoder.
459-
private struct TextSingleValueEncodingContainer: SingleValueEncodingContainer {
460-
let outputStream: OutputByteStream
461-
let formattingOptions: PlainTextEncoder.FormattingOptions
462-
let userInfo: [CodingUserInfoKey: Any]
463-
let codingPath: [CodingKey]
464-
465-
private mutating func emit(_ value: String) {
466-
outputStream.send("\(String(repeating: " ", count: codingPath.count))\(value)\n")
467-
}
468-
mutating func encodeNil() throws { emit("nil") }
469-
mutating func encode(_ value: Bool) throws { emit("\(value)") }
470-
mutating func encode(_ value: String) throws { emit("\(value)") }
471-
mutating func encode(_ value: Double) throws { emit("\(value)") }
472-
mutating func encode(_ value: Float) throws { emit("\(value)") }
473-
mutating func encode(_ value: Int) throws { emit("\(value)") }
474-
mutating func encode(_ value: Int8) throws { emit("\(value)") }
475-
mutating func encode(_ value: Int16) throws { emit("\(value)") }
476-
mutating func encode(_ value: Int32) throws { emit("\(value)") }
477-
mutating func encode(_ value: Int64) throws { emit("\(value)") }
478-
mutating func encode(_ value: UInt) throws { emit("\(value)") }
479-
mutating func encode(_ value: UInt8) throws { emit("\(value)") }
480-
mutating func encode(_ value: UInt16) throws { emit("\(value)") }
481-
mutating func encode(_ value: UInt32) throws { emit("\(value)") }
482-
mutating func encode(_ value: UInt64) throws { emit("\(value)") }
483-
mutating func encode<T: Encodable>(_ value: T) throws {
484-
let textEncoder = _PlainTextEncoder(outputStream: outputStream, formattingOptions: formattingOptions, userInfo: userInfo, codingPath: codingPath)
485-
try value.encode(to: textEncoder)
486-
}
487-
}
488-
}
489-
}

0 commit comments

Comments
 (0)