Adding option .directory for scaffolding templates (#2985)
* [WIP] Adding option for scaffolding files * Use of file manager for copying folder, removed recursive copy files * adding documentation in `scaffold.md` and unit test modified for support `.directory` option * Update CHANGELOG.md Co-authored-by: Daniele Formichelli <df@bendingspoons.com> * Adding acceptance testing * changing `Template.File` for `Template.Item` and other comment in PR * Fixing references for items/files in templates Co-authored-by: Daniele Formichelli <df@bendingspoons.com>
This commit is contained in:
parent
52dc811a73
commit
74fbf0179b
|
@ -4,6 +4,10 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/
|
||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add option to `Scaffolding` for copy folder with option `.directory(path: "destinationContainerFolder", sourcePath: "sourceFolder")`. [#2985](https://github.com/tuist/tuist/pull/2985) by [@santi-d](https://github.com/santi-d)
|
||||||
|
|
||||||
## 1.43.0 - Peroxide
|
## 1.43.0 - Peroxide
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -6,20 +6,31 @@ public struct Template: Codable, Equatable {
|
||||||
public let description: String
|
public let description: String
|
||||||
/// Attributes to be passed to template
|
/// Attributes to be passed to template
|
||||||
public let attributes: [Attribute]
|
public let attributes: [Attribute]
|
||||||
/// Files to generate
|
/// Items to generate
|
||||||
public let files: [File]
|
public let items: [Item]
|
||||||
|
|
||||||
public init(description: String,
|
public init(description: String,
|
||||||
attributes: [Attribute] = [],
|
attributes: [Attribute] = [],
|
||||||
files: [File] = [])
|
items: [Item] = [])
|
||||||
{
|
{
|
||||||
self.description = description
|
self.description = description
|
||||||
self.attributes = attributes
|
self.attributes = attributes
|
||||||
self.files = files
|
self.items = items
|
||||||
dumpIfNeeded(self)
|
dumpIfNeeded(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enum containing information about how to generate file
|
@available(*, deprecated, message: "Use init with `items: [Item]` instead")
|
||||||
|
public init(description: String,
|
||||||
|
attributes: [Attribute] = [],
|
||||||
|
files: [Item] = [])
|
||||||
|
{
|
||||||
|
self.description = description
|
||||||
|
self.attributes = attributes
|
||||||
|
items = files
|
||||||
|
dumpIfNeeded(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enum containing information about how to generate item
|
||||||
public enum Contents: Codable, Equatable {
|
public enum Contents: Codable, Equatable {
|
||||||
/// String Contents is defined in `name_of_template.swift` and contains a simple `String`
|
/// String Contents is defined in `name_of_template.swift` and contains a simple `String`
|
||||||
/// Can not contain any additional logic apart from plain `String` from `arguments`
|
/// Can not contain any additional logic apart from plain `String` from `arguments`
|
||||||
|
@ -27,6 +38,9 @@ public struct Template: Codable, Equatable {
|
||||||
/// File content is defined in a different file from `name_of_template.swift`
|
/// File content is defined in a different file from `name_of_template.swift`
|
||||||
/// Can contain additional logic and anything that is defined in `ProjectDescriptionHelpers`
|
/// Can contain additional logic and anything that is defined in `ProjectDescriptionHelpers`
|
||||||
case file(Path)
|
case file(Path)
|
||||||
|
/// Directory content is defined in a path
|
||||||
|
/// It is just for copying files without modifications and logic inside
|
||||||
|
case directory(Path)
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case type
|
case type
|
||||||
|
@ -42,6 +56,9 @@ public struct Template: Codable, Equatable {
|
||||||
} else if type == "file" {
|
} else if type == "file" {
|
||||||
let value = try container.decode(Path.self, forKey: .value)
|
let value = try container.decode(Path.self, forKey: .value)
|
||||||
self = .file(value)
|
self = .file(value)
|
||||||
|
} else if type == "directory" {
|
||||||
|
let value = try container.decode(Path.self, forKey: .value)
|
||||||
|
self = .directory(value)
|
||||||
} else {
|
} else {
|
||||||
fatalError("Argument '\(type)' not supported")
|
fatalError("Argument '\(type)' not supported")
|
||||||
}
|
}
|
||||||
|
@ -57,12 +74,15 @@ public struct Template: Codable, Equatable {
|
||||||
case let .file(path):
|
case let .file(path):
|
||||||
try container.encode("file", forKey: .type)
|
try container.encode("file", forKey: .type)
|
||||||
try container.encode(path, forKey: .value)
|
try container.encode(path, forKey: .value)
|
||||||
|
case let .directory(path):
|
||||||
|
try container.encode("directory", forKey: .type)
|
||||||
|
try container.encode(path, forKey: .value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// File description for generating
|
/// File description for generating
|
||||||
public struct File: Codable, Equatable {
|
public struct Item: Codable, Equatable {
|
||||||
public let path: String
|
public let path: String
|
||||||
public let contents: Contents
|
public let contents: Contents
|
||||||
|
|
||||||
|
@ -115,21 +135,29 @@ public struct Template: Codable, Equatable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Template.File {
|
public extension Template.Item {
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - path: Path where to generate file
|
/// - path: Path where to generate file
|
||||||
/// - contents: String Contents
|
/// - contents: String Contents
|
||||||
/// - Returns: `Template.File` that is `.string`
|
/// - Returns: `Template.Item` that is `.string`
|
||||||
static func string(path: String, contents: String) -> Template.File {
|
static func string(path: String, contents: String) -> Template.Item {
|
||||||
Template.File(path: path, contents: .string(contents))
|
Template.Item(path: path, contents: .string(contents))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - path: Path where to generate file
|
/// - path: Path where to generate file
|
||||||
/// - templatePath: Path of file where the template is defined
|
/// - templatePath: Path of file where the template is defined
|
||||||
/// - Returns: `Template.File` that is `.file`
|
/// - Returns: `Template.Item` that is `.file`
|
||||||
static func file(path: String, templatePath: Path) -> Template.File {
|
static func file(path: String, templatePath: Path) -> Template.Item {
|
||||||
Template.File(path: path, contents: .file(templatePath))
|
Template.Item(path: path, contents: .file(templatePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// - Parameters:
|
||||||
|
/// - path: Path where will be copied the folder
|
||||||
|
/// - sourcePath: Path of folder which will be copied
|
||||||
|
/// - Returns: `Template.Item` that is `.directory`
|
||||||
|
static func directory(path: String, sourcePath: Path) -> Template.Item {
|
||||||
|
Template.Item(path: path, contents: .directory(sourcePath))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,15 +3,27 @@ import TSCBasic
|
||||||
public struct Template: Equatable {
|
public struct Template: Equatable {
|
||||||
public let description: String
|
public let description: String
|
||||||
public let attributes: [Attribute]
|
public let attributes: [Attribute]
|
||||||
public let files: [File]
|
public let items: [Item]
|
||||||
|
|
||||||
public init(description: String,
|
public init(description: String,
|
||||||
attributes: [Attribute] = [],
|
attributes: [Attribute] = [],
|
||||||
files: [File] = [])
|
items: [Item] = [])
|
||||||
{
|
{
|
||||||
self.description = description
|
self.description = description
|
||||||
self.attributes = attributes
|
self.attributes = attributes
|
||||||
self.files = files
|
self.items = items
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, deprecated, message: "Use init with `items: [Item]` instead")
|
||||||
|
public init(description: String,
|
||||||
|
attributes: [Attribute] = [],
|
||||||
|
files: [Item] = [])
|
||||||
|
{
|
||||||
|
self.init(
|
||||||
|
description: description,
|
||||||
|
attributes: attributes,
|
||||||
|
items: files
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Attribute: Equatable {
|
public enum Attribute: Equatable {
|
||||||
|
@ -40,9 +52,10 @@ public struct Template: Equatable {
|
||||||
public enum Contents: Equatable {
|
public enum Contents: Equatable {
|
||||||
case string(String)
|
case string(String)
|
||||||
case file(AbsolutePath)
|
case file(AbsolutePath)
|
||||||
|
case directory(AbsolutePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct File: Equatable {
|
public struct Item: Equatable {
|
||||||
public let path: RelativePath
|
public let path: RelativePath
|
||||||
public let contents: Contents
|
public let contents: Contents
|
||||||
|
|
||||||
|
|
|
@ -5,21 +5,21 @@ import TSCBasic
|
||||||
extension Template {
|
extension Template {
|
||||||
public static func test(description: String = "Template",
|
public static func test(description: String = "Template",
|
||||||
attributes: [Attribute] = [],
|
attributes: [Attribute] = [],
|
||||||
files: [Template.File] = []) -> Template
|
items: [Template.Item] = []) -> Template
|
||||||
{
|
{
|
||||||
Template(
|
Template(
|
||||||
description: description,
|
description: description,
|
||||||
attributes: attributes,
|
attributes: attributes,
|
||||||
files: files
|
items: items
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Template.File {
|
extension Template.Item {
|
||||||
public static func test(path: RelativePath,
|
public static func test(path: RelativePath,
|
||||||
contents: Template.Contents = .string("test content")) -> Template.File
|
contents: Template.Contents = .string("test content")) -> Template.Item
|
||||||
{
|
{
|
||||||
Template.File(
|
Template.Item(
|
||||||
path: path,
|
path: path,
|
||||||
contents: contents
|
contents: contents
|
||||||
)
|
)
|
||||||
|
|
|
@ -38,7 +38,7 @@ public class TemplateLoader: TemplateLoading {
|
||||||
extension TuistGraph.Template {
|
extension TuistGraph.Template {
|
||||||
static func from(manifest: ProjectDescription.Template, generatorPaths: GeneratorPaths) throws -> TuistGraph.Template {
|
static func from(manifest: ProjectDescription.Template, generatorPaths: GeneratorPaths) throws -> TuistGraph.Template {
|
||||||
let attributes = try manifest.attributes.map(TuistGraph.Template.Attribute.from)
|
let attributes = try manifest.attributes.map(TuistGraph.Template.Attribute.from)
|
||||||
let files = try manifest.files.map { File(
|
let items = try manifest.items.map { Item(
|
||||||
path: RelativePath($0.path),
|
path: RelativePath($0.path),
|
||||||
contents: try TuistGraph.Template.Contents.from(
|
contents: try TuistGraph.Template.Contents.from(
|
||||||
manifest: $0.contents,
|
manifest: $0.contents,
|
||||||
|
@ -48,7 +48,7 @@ extension TuistGraph.Template {
|
||||||
return TuistGraph.Template(
|
return TuistGraph.Template(
|
||||||
description: manifest.description,
|
description: manifest.description,
|
||||||
attributes: attributes,
|
attributes: attributes,
|
||||||
files: files
|
items: items
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,6 +73,8 @@ extension TuistGraph.Template.Contents {
|
||||||
return .string(contents)
|
return .string(contents)
|
||||||
case let .file(templatePath):
|
case let .file(templatePath):
|
||||||
return .file(try generatorPaths.resolve(path: templatePath))
|
return .file(try generatorPaths.resolve(path: templatePath))
|
||||||
|
case let .directory(sourcePath):
|
||||||
|
return .directory(try generatorPaths.resolve(path: sourcePath))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,6 @@ import TuistLoader
|
||||||
public final class MockTemplateLoader: TemplateLoading {
|
public final class MockTemplateLoader: TemplateLoading {
|
||||||
public var loadTemplateStub: ((AbsolutePath) throws -> Template)?
|
public var loadTemplateStub: ((AbsolutePath) throws -> Template)?
|
||||||
public func loadTemplate(at path: AbsolutePath) throws -> Template {
|
public func loadTemplate(at path: AbsolutePath) throws -> Template {
|
||||||
try loadTemplateStub?(path) ?? Template(description: "", attributes: [], files: [])
|
try loadTemplateStub?(path) ?? Template(description: "", attributes: [], items: [])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,12 +12,12 @@ extension Config {
|
||||||
extension Template {
|
extension Template {
|
||||||
public static func test(description: String = "Template",
|
public static func test(description: String = "Template",
|
||||||
attributes: [Attribute] = [],
|
attributes: [Attribute] = [],
|
||||||
files: [Template.File] = []) -> Template
|
items: [Template.Item] = []) -> Template
|
||||||
{
|
{
|
||||||
Template(
|
Template(
|
||||||
description: description,
|
description: description,
|
||||||
attributes: attributes,
|
attributes: attributes,
|
||||||
files: files
|
items: items
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,17 +25,17 @@ public final class TemplateGenerator: TemplateGenerating {
|
||||||
to destinationPath: AbsolutePath,
|
to destinationPath: AbsolutePath,
|
||||||
attributes: [String: String]) throws
|
attributes: [String: String]) throws
|
||||||
{
|
{
|
||||||
let renderedFiles = renderFiles(
|
let renderedItems = renderItems(
|
||||||
template: template,
|
template: template,
|
||||||
attributes: attributes
|
attributes: attributes
|
||||||
)
|
)
|
||||||
try generateDirectories(
|
try generateDirectories(
|
||||||
renderedFiles: renderedFiles,
|
renderedItems: renderedItems,
|
||||||
destinationPath: destinationPath
|
destinationPath: destinationPath
|
||||||
)
|
)
|
||||||
|
|
||||||
try generateFiles(
|
try generateItems(
|
||||||
renderedFiles: renderedFiles,
|
renderedItems: renderedItems,
|
||||||
attributes: attributes,
|
attributes: attributes,
|
||||||
destinationPath: destinationPath
|
destinationPath: destinationPath
|
||||||
)
|
)
|
||||||
|
@ -43,12 +43,12 @@ public final class TemplateGenerator: TemplateGenerating {
|
||||||
|
|
||||||
// MARK: - Helpers
|
// MARK: - Helpers
|
||||||
|
|
||||||
/// Renders files' paths in format path_to_dir/{{ attribute_name }} with `attributes`
|
/// Renders items' paths in format path_to_dir/{{ attribute_name }} with `attributes`
|
||||||
private func renderFiles(template: Template,
|
private func renderItems(template: Template,
|
||||||
attributes: [String: String]) -> [Template.File]
|
attributes: [String: String]) -> [Template.Item]
|
||||||
{
|
{
|
||||||
attributes.reduce(template.files) { files, attribute in
|
attributes.reduce(template.items) { items, attribute in
|
||||||
files.map {
|
items.map {
|
||||||
let path = RelativePath($0.path.pathString.replacingOccurrences(of: "{{ \(attribute.key) }}", with: attribute.value))
|
let path = RelativePath($0.path.pathString.replacingOccurrences(of: "{{ \(attribute.key) }}", with: attribute.value))
|
||||||
|
|
||||||
var contents = $0.contents
|
var contents = $0.contents
|
||||||
|
@ -62,16 +62,16 @@ public final class TemplateGenerator: TemplateGenerating {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Template.File(path: path, contents: contents)
|
return Template.Item(path: path, contents: contents)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate all necessary directories
|
/// Generate all necessary directories
|
||||||
private func generateDirectories(renderedFiles: [Template.File],
|
private func generateDirectories(renderedItems: [Template.Item],
|
||||||
destinationPath: AbsolutePath) throws
|
destinationPath: AbsolutePath) throws
|
||||||
{
|
{
|
||||||
try renderedFiles
|
try renderedItems
|
||||||
.map(\.path)
|
.map(\.path)
|
||||||
.map {
|
.map {
|
||||||
destinationPath.appending(RelativePath($0.dirname))
|
destinationPath.appending(RelativePath($0.dirname))
|
||||||
|
@ -82,14 +82,14 @@ public final class TemplateGenerator: TemplateGenerating {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate all `renderedFiles`
|
/// Generate all `renderedItems`
|
||||||
private func generateFiles(renderedFiles: [Template.File],
|
private func generateItems(renderedItems: [Template.Item],
|
||||||
attributes: [String: String],
|
attributes: [String: String],
|
||||||
destinationPath: AbsolutePath) throws
|
destinationPath: AbsolutePath) throws
|
||||||
{
|
{
|
||||||
let environment = stencilSwiftEnvironment()
|
let environment = stencilSwiftEnvironment()
|
||||||
try renderedFiles.forEach {
|
try renderedItems.forEach {
|
||||||
let renderedContents: String
|
let renderedContents: String?
|
||||||
switch $0.contents {
|
switch $0.contents {
|
||||||
case let .string(contents):
|
case let .string(contents):
|
||||||
renderedContents = try environment.renderTemplate(
|
renderedContents = try environment.renderTemplate(
|
||||||
|
@ -107,11 +107,22 @@ public final class TemplateGenerator: TemplateGenerating {
|
||||||
} else {
|
} else {
|
||||||
renderedContents = fileContents
|
renderedContents = fileContents
|
||||||
}
|
}
|
||||||
|
case let .directory(path):
|
||||||
|
let destinationDirectoryPath = destinationPath.appending(components: $0.path.pathString, path.basename)
|
||||||
|
// workaround for creating folder tree of destinationDirectoryPath
|
||||||
|
if !FileHandler.shared.exists(destinationDirectoryPath.parentDirectory) {
|
||||||
|
try FileHandler.shared.createFolder(destinationDirectoryPath.parentDirectory)
|
||||||
|
}
|
||||||
|
if FileHandler.shared.exists(destinationDirectoryPath) {
|
||||||
|
try FileHandler.shared.delete(destinationDirectoryPath)
|
||||||
|
}
|
||||||
|
try FileHandler.shared.copy(from: path, to: destinationDirectoryPath)
|
||||||
|
renderedContents = nil
|
||||||
}
|
}
|
||||||
// Generate file only when it has some content
|
// Generate file only when it has some content
|
||||||
guard !renderedContents.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
|
guard let rendered = renderedContents, !rendered.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
|
||||||
try FileHandler.shared.write(
|
try FileHandler.shared.write(
|
||||||
renderedContents,
|
rendered,
|
||||||
path: destinationPath.appending($0.path),
|
path: destinationPath.appending($0.path),
|
||||||
atomically: true
|
atomically: true
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,9 +12,10 @@ class TemplateTests: XCTestCase {
|
||||||
.optional("aName", default: "defaultName"),
|
.optional("aName", default: "defaultName"),
|
||||||
.optional("bName", default: ""),
|
.optional("bName", default: ""),
|
||||||
],
|
],
|
||||||
files: [
|
items: [
|
||||||
.string(path: "static.swift", contents: "content"),
|
.string(path: "static.swift", contents: "content"),
|
||||||
.file(path: "generated.swift", templatePath: "generate.swift"),
|
.file(path: "generated.swift", templatePath: "generate.swift"),
|
||||||
|
.directory(path: "destinationFolder", sourcePath: "sourceFolder"),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,7 @@ final class ListServiceTests: TuistUnitTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
templateLoader.loadTemplateStub = { _ in
|
templateLoader.loadTemplateStub = { _ in
|
||||||
Template(description: "description")
|
Template(description: "description", items: [])
|
||||||
}
|
}
|
||||||
|
|
||||||
// When
|
// When
|
||||||
|
@ -82,7 +82,7 @@ final class ListServiceTests: TuistUnitTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
templateLoader.loadTemplateStub = { _ in
|
templateLoader.loadTemplateStub = { _ in
|
||||||
Template(description: "description")
|
Template(description: "description", items: [])
|
||||||
}
|
}
|
||||||
|
|
||||||
// When
|
// When
|
||||||
|
@ -113,7 +113,7 @@ final class ListServiceTests: TuistUnitTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
templateLoader.loadTemplateStub = { _ in
|
templateLoader.loadTemplateStub = { _ in
|
||||||
Template(description: "description")
|
Template(description: "description", items: [])
|
||||||
}
|
}
|
||||||
|
|
||||||
// When
|
// When
|
||||||
|
|
|
@ -54,7 +54,8 @@ final class ScaffoldServiceTests: TuistUnitTestCase {
|
||||||
attributes: [
|
attributes: [
|
||||||
.required("required"),
|
.required("required"),
|
||||||
.optional("optional", default: ""),
|
.optional("optional", default: ""),
|
||||||
]
|
],
|
||||||
|
items: []
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,7 +84,8 @@ final class ScaffoldServiceTests: TuistUnitTestCase {
|
||||||
attributes: [
|
attributes: [
|
||||||
.required("required"),
|
.required("required"),
|
||||||
.optional("optional", default: ""),
|
.optional("optional", default: ""),
|
||||||
]
|
],
|
||||||
|
items: []
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -132,7 +132,8 @@ final class ManifestLoaderTests: TuistTestCase {
|
||||||
import ProjectDescription
|
import ProjectDescription
|
||||||
|
|
||||||
let template = Template(
|
let template = Template(
|
||||||
description: "Template description"
|
description: "Template description",
|
||||||
|
items: []
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -158,7 +159,8 @@ final class ManifestLoaderTests: TuistTestCase {
|
||||||
import ProjectDescription
|
import ProjectDescription
|
||||||
|
|
||||||
let template = Template(
|
let template = Template(
|
||||||
description: "Template description"
|
description: "Template description",
|
||||||
|
items: []
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,7 @@ final class TemplateLoaderTests: TuistUnitTestCase {
|
||||||
manifestLoader.loadTemplateStub = { _ in
|
manifestLoader.loadTemplateStub = { _ in
|
||||||
ProjectDescription.Template(
|
ProjectDescription.Template(
|
||||||
description: "desc",
|
description: "desc",
|
||||||
files: [ProjectDescription.Template.File(
|
items: [ProjectDescription.Template.Item(
|
||||||
path: "generateOne",
|
path: "generateOne",
|
||||||
contents: .file("fileOne")
|
contents: .file("fileOne")
|
||||||
)]
|
)]
|
||||||
|
@ -58,7 +58,7 @@ final class TemplateLoaderTests: TuistUnitTestCase {
|
||||||
// Then
|
// Then
|
||||||
XCTAssertEqual(got, TuistGraph.Template(
|
XCTAssertEqual(got, TuistGraph.Template(
|
||||||
description: "desc",
|
description: "desc",
|
||||||
files: [Template.File(
|
items: [Template.Item(
|
||||||
path: RelativePath("generateOne"),
|
path: RelativePath("generateOne"),
|
||||||
contents: .file(temporaryPath.appending(component: "fileOne"))
|
contents: .file(temporaryPath.appending(component: "fileOne"))
|
||||||
)]
|
)]
|
||||||
|
|
|
@ -27,11 +27,11 @@ final class TemplateGeneratorTests: TuistTestCase {
|
||||||
let directories = [RelativePath("a"), RelativePath("a/b"), RelativePath("c")]
|
let directories = [RelativePath("a"), RelativePath("a/b"), RelativePath("c")]
|
||||||
let destinationPath = try temporaryPath()
|
let destinationPath = try temporaryPath()
|
||||||
let expectedDirectories = directories.map(destinationPath.appending)
|
let expectedDirectories = directories.map(destinationPath.appending)
|
||||||
let files = directories.map {
|
let items = directories.map {
|
||||||
Template.File.test(path: RelativePath($0.pathString + "/file.swift"))
|
Template.Item.test(path: RelativePath($0.pathString + "/file.swift"))
|
||||||
}
|
}
|
||||||
|
|
||||||
let template = Template.test(files: files)
|
let template = Template.test(items: items)
|
||||||
|
|
||||||
// When
|
// When
|
||||||
try subject.generate(
|
try subject.generate(
|
||||||
|
@ -47,10 +47,10 @@ final class TemplateGeneratorTests: TuistTestCase {
|
||||||
func test_directories_with_attributes() throws {
|
func test_directories_with_attributes() throws {
|
||||||
// Given
|
// Given
|
||||||
let directories = [RelativePath("{{ name }}"), RelativePath("{{ aName }}"), RelativePath("{{ name }}/{{ bName }}")]
|
let directories = [RelativePath("{{ name }}"), RelativePath("{{ aName }}"), RelativePath("{{ name }}/{{ bName }}")]
|
||||||
let files = directories.map {
|
let items = directories.map {
|
||||||
Template.File.test(path: RelativePath($0.pathString + "/file.swift"))
|
Template.Item.test(path: RelativePath($0.pathString + "/file.swift"))
|
||||||
}
|
}
|
||||||
let template = Template.test(files: files)
|
let template = Template.test(items: items)
|
||||||
let destinationPath = try temporaryPath()
|
let destinationPath = try temporaryPath()
|
||||||
let expectedDirectories = [RelativePath("test_name"),
|
let expectedDirectories = [RelativePath("test_name"),
|
||||||
RelativePath("test"),
|
RelativePath("test"),
|
||||||
|
@ -71,19 +71,19 @@ final class TemplateGeneratorTests: TuistTestCase {
|
||||||
|
|
||||||
func test_files_are_generated() throws {
|
func test_files_are_generated() throws {
|
||||||
// Given
|
// Given
|
||||||
let files: [Template.File] = [
|
let items: [Template.Item] = [
|
||||||
Template.File(path: RelativePath("a"), contents: .string("aContent")),
|
Template.Item(path: RelativePath("a"), contents: .string("aContent")),
|
||||||
Template.File(path: RelativePath("b"), contents: .string("bContent")),
|
Template.Item(path: RelativePath("b"), contents: .string("bContent")),
|
||||||
]
|
]
|
||||||
|
|
||||||
let template = Template.test(files: files)
|
let template = Template.test(items: items)
|
||||||
let destinationPath = try temporaryPath()
|
let destinationPath = try temporaryPath()
|
||||||
let expectedFiles: [(AbsolutePath, String)] = files.compactMap {
|
let expectedFiles: [(AbsolutePath, String)] = items.compactMap {
|
||||||
let content: String
|
let content: String
|
||||||
switch $0.contents {
|
switch $0.contents {
|
||||||
case let .string(staticContent):
|
case let .string(staticContent):
|
||||||
content = staticContent
|
content = staticContent
|
||||||
case .file:
|
case .file, .directory:
|
||||||
XCTFail("Unexpected type")
|
XCTFail("Unexpected type")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -106,12 +106,12 @@ final class TemplateGeneratorTests: TuistTestCase {
|
||||||
func test_files_are_generated_with_attributes() throws {
|
func test_files_are_generated_with_attributes() throws {
|
||||||
// Given
|
// Given
|
||||||
let sourcePath = try temporaryPath()
|
let sourcePath = try temporaryPath()
|
||||||
let files = [
|
let items = [
|
||||||
Template.File(path: RelativePath("{{ name }}"), contents: .string("{{ contentName }}")),
|
Template.Item(path: RelativePath("{{ name }}"), contents: .string("{{ contentName }}")),
|
||||||
Template.File(path: RelativePath("{{ directoryName }}/{{ fileName }}"), contents: .string("bContent")),
|
Template.Item(path: RelativePath("{{ directoryName }}/{{ fileName }}"), contents: .string("bContent")),
|
||||||
Template.File(path: RelativePath("file"), contents: .file(sourcePath.appending(component: "{{ filePath }}"))),
|
Template.Item(path: RelativePath("file"), contents: .file(sourcePath.appending(component: "{{ filePath }}"))),
|
||||||
]
|
]
|
||||||
let template = Template.test(files: files)
|
let template = Template.test(items: items)
|
||||||
let name = "test name"
|
let name = "test name"
|
||||||
let contentName = "test content"
|
let contentName = "test content"
|
||||||
let fileContent = "test file content"
|
let fileContent = "test file content"
|
||||||
|
@ -152,11 +152,11 @@ final class TemplateGeneratorTests: TuistTestCase {
|
||||||
let name = "test name"
|
let name = "test name"
|
||||||
let aContent = "test a content"
|
let aContent = "test a content"
|
||||||
let bContent = "test b content"
|
let bContent = "test b content"
|
||||||
let files = [
|
let items = [
|
||||||
Template.File(path: RelativePath("a/file"), contents: .file(sourcePath.appending(component: "testFile"))),
|
Template.Item(path: RelativePath("a/file"), contents: .file(sourcePath.appending(component: "testFile"))),
|
||||||
Template.File(path: RelativePath("b/{{ name }}/file"), contents: .file(sourcePath.appending(components: "bTestFile"))),
|
Template.Item(path: RelativePath("b/{{ name }}/file"), contents: .file(sourcePath.appending(components: "bTestFile"))),
|
||||||
]
|
]
|
||||||
let template = Template.test(files: files)
|
let template = Template.test(items: items)
|
||||||
try FileHandler.shared.write(aContent, path: sourcePath.appending(component: "testFile"), atomically: true)
|
try FileHandler.shared.write(aContent, path: sourcePath.appending(component: "testFile"), atomically: true)
|
||||||
try FileHandler.shared.write(bContent, path: sourcePath.appending(component: "bTestFile"), atomically: true)
|
try FileHandler.shared.write(bContent, path: sourcePath.appending(component: "bTestFile"), atomically: true)
|
||||||
let expectedFiles: [(AbsolutePath, String)] = [
|
let expectedFiles: [(AbsolutePath, String)] = [
|
||||||
|
@ -187,7 +187,7 @@ final class TemplateGeneratorTests: TuistTestCase {
|
||||||
path: sourcePath.appending(component: "a.stencil"),
|
path: sourcePath.appending(component: "a.stencil"),
|
||||||
atomically: true
|
atomically: true
|
||||||
)
|
)
|
||||||
let template = Template.test(files: [Template.File(
|
let template = Template.test(items: [Template.Item(
|
||||||
path: RelativePath("a"),
|
path: RelativePath("a"),
|
||||||
contents: .file(sourcePath.appending(component: "a.stencil"))
|
contents: .file(sourcePath.appending(component: "a.stencil"))
|
||||||
)])
|
)])
|
||||||
|
@ -222,12 +222,12 @@ final class TemplateGeneratorTests: TuistTestCase {
|
||||||
path: sourcePath.appending(component: "a.swift"),
|
path: sourcePath.appending(component: "a.swift"),
|
||||||
atomically: true
|
atomically: true
|
||||||
)
|
)
|
||||||
let template = Template.test(files: [
|
let template = Template.test(items: [
|
||||||
Template.File(
|
Template.Item(
|
||||||
path: RelativePath("unrendered"),
|
path: RelativePath("unrendered"),
|
||||||
contents: .file(sourcePath.appending(component: "a.swift"))
|
contents: .file(sourcePath.appending(component: "a.swift"))
|
||||||
),
|
),
|
||||||
Template.File(
|
Template.Item(
|
||||||
path: RelativePath("rendered"),
|
path: RelativePath("rendered"),
|
||||||
contents: .file(sourcePath.appending(component: "a.stencil"))
|
contents: .file(sourcePath.appending(component: "a.stencil"))
|
||||||
),
|
),
|
||||||
|
@ -260,8 +260,8 @@ final class TemplateGeneratorTests: TuistTestCase {
|
||||||
path: sourcePath.appending(component: "b.stencil"),
|
path: sourcePath.appending(component: "b.stencil"),
|
||||||
atomically: true
|
atomically: true
|
||||||
)
|
)
|
||||||
let template = Template.test(files: [
|
let template = Template.test(items: [
|
||||||
Template.File(
|
Template.Item(
|
||||||
path: RelativePath("ignore"),
|
path: RelativePath("ignore"),
|
||||||
contents: .file(sourcePath.appending(component: "b.stencil"))
|
contents: .file(sourcePath.appending(component: "b.stencil"))
|
||||||
),
|
),
|
||||||
|
@ -277,4 +277,39 @@ final class TemplateGeneratorTests: TuistTestCase {
|
||||||
// Then
|
// Then
|
||||||
XCTAssertFalse(FileHandler.shared.exists(destinationPath.appending(component: "ignore")))
|
XCTAssertFalse(FileHandler.shared.exists(destinationPath.appending(component: "ignore")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func test_copy_directory() throws {
|
||||||
|
// Given
|
||||||
|
let sourcePath = try temporaryPath().appending(components: "folder")
|
||||||
|
try FileHandler.shared.createFolder(sourcePath)
|
||||||
|
|
||||||
|
let destinationPath = try temporaryPath()
|
||||||
|
let expectedContentFile = "File's content"
|
||||||
|
|
||||||
|
try FileHandler.shared.write(
|
||||||
|
expectedContentFile,
|
||||||
|
path: sourcePath.appending(component: "file1.txt"),
|
||||||
|
atomically: true
|
||||||
|
)
|
||||||
|
|
||||||
|
let template = Template.test(items: [
|
||||||
|
Template.Item(
|
||||||
|
path: RelativePath("destination"),
|
||||||
|
contents: .directory(sourcePath)
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
// When
|
||||||
|
try subject.generate(
|
||||||
|
template: template,
|
||||||
|
to: destinationPath,
|
||||||
|
attributes: [:]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
XCTAssertEqual(
|
||||||
|
try FileHandler.shared.readTextFile(destinationPath.appending(components: "destination", "folder", "file1.txt")),
|
||||||
|
expectedContentFile
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,10 @@ let template = Template(
|
||||||
path: "generated/Up.swift",
|
path: "generated/Up.swift",
|
||||||
templatePath: "generate.stencil"
|
templatePath: "generate.stencil"
|
||||||
),
|
),
|
||||||
|
.directory(
|
||||||
|
path: "destinationFolder",
|
||||||
|
sourcePath: "sourceFolder"
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
@ -54,4 +58,6 @@ Since platform is an optional argument, we can also call the command without the
|
||||||
|
|
||||||
If `.string` and `.files` don't provide enough flexibility, you can leverage the [Stencil](https://github.com/stencilproject/Stencil) templating language via the `.file` case. Besides that, you can also use additional filters defined [here](https://github.com/SwiftGen/StencilSwiftKit#filters)
|
If `.string` and `.files` don't provide enough flexibility, you can leverage the [Stencil](https://github.com/stencilproject/Stencil) templating language via the `.file` case. Besides that, you can also use additional filters defined [here](https://github.com/SwiftGen/StencilSwiftKit#filters)
|
||||||
|
|
||||||
|
You can also use `.directory` which gives the possibility to copy entire folders to a given path.
|
||||||
|
|
||||||
Templates can import [project description helpers](/guides/helpers/). Just add `import ProjectDescriptionHelpers` at the top, and extract reusable logic into the helpers.
|
Templates can import [project description helpers](/guides/helpers/). Just add `import ProjectDescriptionHelpers` at the top, and extract reusable logic into the helpers.
|
||||||
|
|
|
@ -25,6 +25,23 @@ Feature: Scaffold a project using Tuist
|
||||||
"""
|
"""
|
||||||
// Generated file with platform: iOS and snake case name: template_project
|
// Generated file with platform: iOS and snake case name: template_project
|
||||||
|
|
||||||
|
"""
|
||||||
|
Then tuist scaffolds a custom_using_copy_folder template to TemplateProject named TemplateProject
|
||||||
|
Then content of a file named TemplateProject/custom.swift in a directory TemplateProject should be equal to // this is test TemplateProject content
|
||||||
|
Then content of a file named TemplateProject/generated.swift in a directory TemplateProject should be equal to:
|
||||||
|
"""
|
||||||
|
// Generated file with platform: ios and name: TemplateProject
|
||||||
|
|
||||||
|
"""
|
||||||
|
Then content of a file named TemplateProject/sourceFolder/file1.txt in a directory TemplateProject should be equal to:
|
||||||
|
"""
|
||||||
|
Content of file 1
|
||||||
|
|
||||||
|
"""
|
||||||
|
Then content of a file named TemplateProject/sourceFolder/subFolder/file2.txt in a directory TemplateProject should be equal to:
|
||||||
|
"""
|
||||||
|
Content of file 2
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Scenario: The project is an application with templates from plugins (app_with_plugins)
|
Scenario: The project is an application with templates from plugins (app_with_plugins)
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import ProjectDescription
|
||||||
|
|
||||||
|
let nameAttributeFour: Template.Attribute = .required("name")
|
||||||
|
let platformAttributeFour: Template.Attribute = .optional("platform", default: "ios")
|
||||||
|
|
||||||
|
let testContentsFour = """
|
||||||
|
// this is test \(nameAttributeFour) content
|
||||||
|
"""
|
||||||
|
|
||||||
|
let templateFour = Template(
|
||||||
|
description: "Custom template",
|
||||||
|
attributes: [
|
||||||
|
nameAttributeFour,
|
||||||
|
platformAttributeFour
|
||||||
|
],
|
||||||
|
files: [
|
||||||
|
.string(path: "\(nameAttributeFour)/custom.swift", contents: testContentsFour),
|
||||||
|
.file(path: "\(nameAttributeFour)/generated.swift", templatePath: "platform_four.stencil"),
|
||||||
|
.directory(path: "\(nameAttributeFour)",sourcePath: "sourceFolder")
|
||||||
|
]
|
||||||
|
)
|
|
@ -0,0 +1 @@
|
||||||
|
// Generated file with platform: {{ platform }} and name: {{ name }}
|
|
@ -0,0 +1 @@
|
||||||
|
Content of file 1
|
|
@ -0,0 +1 @@
|
||||||
|
Content of file 2
|
Loading…
Reference in New Issue