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:
Santiago A. Delgado 2021-05-25 05:32:03 -05:00 committed by GitHub
parent 52dc811a73
commit 74fbf0179b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 227 additions and 82 deletions

View File

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

View File

@ -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))
} }
} }

View File

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

View File

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

View File

@ -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))
} }
} }
} }

View File

@ -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: [])
} }
} }

View File

@ -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
) )
} }
} }

View File

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

View File

@ -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"),
] ]
) )

View File

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

View File

@ -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: []
) )
} }

View File

@ -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: []
) )
""" """

View File

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

View File

@ -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
)
}
} }

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
// Generated file with platform: {{ platform }} and name: {{ name }}