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
|
||||
|
||||
### 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
|
||||
|
||||
### Added
|
||||
|
|
|
@ -6,20 +6,31 @@ public struct Template: Codable, Equatable {
|
|||
public let description: String
|
||||
/// Attributes to be passed to template
|
||||
public let attributes: [Attribute]
|
||||
/// Files to generate
|
||||
public let files: [File]
|
||||
/// Items to generate
|
||||
public let items: [Item]
|
||||
|
||||
public init(description: String,
|
||||
attributes: [Attribute] = [],
|
||||
files: [File] = [])
|
||||
items: [Item] = [])
|
||||
{
|
||||
self.description = description
|
||||
self.attributes = attributes
|
||||
self.files = files
|
||||
self.items = items
|
||||
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 {
|
||||
/// 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`
|
||||
|
@ -27,6 +38,9 @@ public struct Template: Codable, Equatable {
|
|||
/// File content is defined in a different file from `name_of_template.swift`
|
||||
/// Can contain additional logic and anything that is defined in `ProjectDescriptionHelpers`
|
||||
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 {
|
||||
case type
|
||||
|
@ -42,6 +56,9 @@ public struct Template: Codable, Equatable {
|
|||
} else if type == "file" {
|
||||
let value = try container.decode(Path.self, forKey: .value)
|
||||
self = .file(value)
|
||||
} else if type == "directory" {
|
||||
let value = try container.decode(Path.self, forKey: .value)
|
||||
self = .directory(value)
|
||||
} else {
|
||||
fatalError("Argument '\(type)' not supported")
|
||||
}
|
||||
|
@ -57,12 +74,15 @@ public struct Template: Codable, Equatable {
|
|||
case let .file(path):
|
||||
try container.encode("file", forKey: .type)
|
||||
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
|
||||
public struct File: Codable, Equatable {
|
||||
public struct Item: Codable, Equatable {
|
||||
public let path: String
|
||||
public let contents: Contents
|
||||
|
||||
|
@ -115,21 +135,29 @@ public struct Template: Codable, Equatable {
|
|||
}
|
||||
}
|
||||
|
||||
public extension Template.File {
|
||||
public extension Template.Item {
|
||||
/// - Parameters:
|
||||
/// - path: Path where to generate file
|
||||
/// - contents: String Contents
|
||||
/// - Returns: `Template.File` that is `.string`
|
||||
static func string(path: String, contents: String) -> Template.File {
|
||||
Template.File(path: path, contents: .string(contents))
|
||||
/// - Returns: `Template.Item` that is `.string`
|
||||
static func string(path: String, contents: String) -> Template.Item {
|
||||
Template.Item(path: path, contents: .string(contents))
|
||||
}
|
||||
|
||||
/// - Parameters:
|
||||
/// - path: Path where to generate file
|
||||
/// - templatePath: Path of file where the template is defined
|
||||
/// - Returns: `Template.File` that is `.file`
|
||||
static func file(path: String, templatePath: Path) -> Template.File {
|
||||
Template.File(path: path, contents: .file(templatePath))
|
||||
/// - Returns: `Template.Item` that is `.file`
|
||||
static func file(path: String, templatePath: Path) -> Template.Item {
|
||||
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 let description: String
|
||||
public let attributes: [Attribute]
|
||||
public let files: [File]
|
||||
public let items: [Item]
|
||||
|
||||
public init(description: String,
|
||||
attributes: [Attribute] = [],
|
||||
files: [File] = [])
|
||||
items: [Item] = [])
|
||||
{
|
||||
self.description = description
|
||||
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 {
|
||||
|
@ -40,9 +52,10 @@ public struct Template: Equatable {
|
|||
public enum Contents: Equatable {
|
||||
case string(String)
|
||||
case file(AbsolutePath)
|
||||
case directory(AbsolutePath)
|
||||
}
|
||||
|
||||
public struct File: Equatable {
|
||||
public struct Item: Equatable {
|
||||
public let path: RelativePath
|
||||
public let contents: Contents
|
||||
|
||||
|
|
|
@ -5,21 +5,21 @@ import TSCBasic
|
|||
extension Template {
|
||||
public static func test(description: String = "Template",
|
||||
attributes: [Attribute] = [],
|
||||
files: [Template.File] = []) -> Template
|
||||
items: [Template.Item] = []) -> Template
|
||||
{
|
||||
Template(
|
||||
description: description,
|
||||
attributes: attributes,
|
||||
files: files
|
||||
items: items
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Template.File {
|
||||
extension Template.Item {
|
||||
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,
|
||||
contents: contents
|
||||
)
|
||||
|
|
|
@ -38,7 +38,7 @@ public class TemplateLoader: TemplateLoading {
|
|||
extension TuistGraph.Template {
|
||||
static func from(manifest: ProjectDescription.Template, generatorPaths: GeneratorPaths) throws -> TuistGraph.Template {
|
||||
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),
|
||||
contents: try TuistGraph.Template.Contents.from(
|
||||
manifest: $0.contents,
|
||||
|
@ -48,7 +48,7 @@ extension TuistGraph.Template {
|
|||
return TuistGraph.Template(
|
||||
description: manifest.description,
|
||||
attributes: attributes,
|
||||
files: files
|
||||
items: items
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -73,6 +73,8 @@ extension TuistGraph.Template.Contents {
|
|||
return .string(contents)
|
||||
case let .file(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 var loadTemplateStub: ((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 {
|
||||
public static func test(description: String = "Template",
|
||||
attributes: [Attribute] = [],
|
||||
files: [Template.File] = []) -> Template
|
||||
items: [Template.Item] = []) -> Template
|
||||
{
|
||||
Template(
|
||||
description: description,
|
||||
attributes: attributes,
|
||||
files: files
|
||||
items: items
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,17 +25,17 @@ public final class TemplateGenerator: TemplateGenerating {
|
|||
to destinationPath: AbsolutePath,
|
||||
attributes: [String: String]) throws
|
||||
{
|
||||
let renderedFiles = renderFiles(
|
||||
let renderedItems = renderItems(
|
||||
template: template,
|
||||
attributes: attributes
|
||||
)
|
||||
try generateDirectories(
|
||||
renderedFiles: renderedFiles,
|
||||
renderedItems: renderedItems,
|
||||
destinationPath: destinationPath
|
||||
)
|
||||
|
||||
try generateFiles(
|
||||
renderedFiles: renderedFiles,
|
||||
try generateItems(
|
||||
renderedItems: renderedItems,
|
||||
attributes: attributes,
|
||||
destinationPath: destinationPath
|
||||
)
|
||||
|
@ -43,12 +43,12 @@ public final class TemplateGenerator: TemplateGenerating {
|
|||
|
||||
// MARK: - Helpers
|
||||
|
||||
/// Renders files' paths in format path_to_dir/{{ attribute_name }} with `attributes`
|
||||
private func renderFiles(template: Template,
|
||||
attributes: [String: String]) -> [Template.File]
|
||||
/// Renders items' paths in format path_to_dir/{{ attribute_name }} with `attributes`
|
||||
private func renderItems(template: Template,
|
||||
attributes: [String: String]) -> [Template.Item]
|
||||
{
|
||||
attributes.reduce(template.files) { files, attribute in
|
||||
files.map {
|
||||
attributes.reduce(template.items) { items, attribute in
|
||||
items.map {
|
||||
let path = RelativePath($0.path.pathString.replacingOccurrences(of: "{{ \(attribute.key) }}", with: attribute.value))
|
||||
|
||||
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
|
||||
private func generateDirectories(renderedFiles: [Template.File],
|
||||
private func generateDirectories(renderedItems: [Template.Item],
|
||||
destinationPath: AbsolutePath) throws
|
||||
{
|
||||
try renderedFiles
|
||||
try renderedItems
|
||||
.map(\.path)
|
||||
.map {
|
||||
destinationPath.appending(RelativePath($0.dirname))
|
||||
|
@ -82,14 +82,14 @@ public final class TemplateGenerator: TemplateGenerating {
|
|||
}
|
||||
}
|
||||
|
||||
/// Generate all `renderedFiles`
|
||||
private func generateFiles(renderedFiles: [Template.File],
|
||||
/// Generate all `renderedItems`
|
||||
private func generateItems(renderedItems: [Template.Item],
|
||||
attributes: [String: String],
|
||||
destinationPath: AbsolutePath) throws
|
||||
{
|
||||
let environment = stencilSwiftEnvironment()
|
||||
try renderedFiles.forEach {
|
||||
let renderedContents: String
|
||||
try renderedItems.forEach {
|
||||
let renderedContents: String?
|
||||
switch $0.contents {
|
||||
case let .string(contents):
|
||||
renderedContents = try environment.renderTemplate(
|
||||
|
@ -107,11 +107,22 @@ public final class TemplateGenerator: TemplateGenerating {
|
|||
} else {
|
||||
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
|
||||
guard !renderedContents.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
|
||||
guard let rendered = renderedContents, !rendered.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
|
||||
try FileHandler.shared.write(
|
||||
renderedContents,
|
||||
rendered,
|
||||
path: destinationPath.appending($0.path),
|
||||
atomically: true
|
||||
)
|
||||
|
|
|
@ -12,9 +12,10 @@ class TemplateTests: XCTestCase {
|
|||
.optional("aName", default: "defaultName"),
|
||||
.optional("bName", default: ""),
|
||||
],
|
||||
files: [
|
||||
items: [
|
||||
.string(path: "static.swift", contents: "content"),
|
||||
.file(path: "generated.swift", templatePath: "generate.swift"),
|
||||
.directory(path: "destinationFolder", sourcePath: "sourceFolder"),
|
||||
]
|
||||
)
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ final class ListServiceTests: TuistUnitTestCase {
|
|||
}
|
||||
|
||||
templateLoader.loadTemplateStub = { _ in
|
||||
Template(description: "description")
|
||||
Template(description: "description", items: [])
|
||||
}
|
||||
|
||||
// When
|
||||
|
@ -82,7 +82,7 @@ final class ListServiceTests: TuistUnitTestCase {
|
|||
}
|
||||
|
||||
templateLoader.loadTemplateStub = { _ in
|
||||
Template(description: "description")
|
||||
Template(description: "description", items: [])
|
||||
}
|
||||
|
||||
// When
|
||||
|
@ -113,7 +113,7 @@ final class ListServiceTests: TuistUnitTestCase {
|
|||
}
|
||||
|
||||
templateLoader.loadTemplateStub = { _ in
|
||||
Template(description: "description")
|
||||
Template(description: "description", items: [])
|
||||
}
|
||||
|
||||
// When
|
||||
|
|
|
@ -54,7 +54,8 @@ final class ScaffoldServiceTests: TuistUnitTestCase {
|
|||
attributes: [
|
||||
.required("required"),
|
||||
.optional("optional", default: ""),
|
||||
]
|
||||
],
|
||||
items: []
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -83,7 +84,8 @@ final class ScaffoldServiceTests: TuistUnitTestCase {
|
|||
attributes: [
|
||||
.required("required"),
|
||||
.optional("optional", default: ""),
|
||||
]
|
||||
],
|
||||
items: []
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -132,7 +132,8 @@ final class ManifestLoaderTests: TuistTestCase {
|
|||
import ProjectDescription
|
||||
|
||||
let template = Template(
|
||||
description: "Template description"
|
||||
description: "Template description",
|
||||
items: []
|
||||
)
|
||||
"""
|
||||
|
||||
|
@ -158,7 +159,8 @@ final class ManifestLoaderTests: TuistTestCase {
|
|||
import ProjectDescription
|
||||
|
||||
let template = Template(
|
||||
description: "Template description"
|
||||
description: "Template description",
|
||||
items: []
|
||||
)
|
||||
"""
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ final class TemplateLoaderTests: TuistUnitTestCase {
|
|||
manifestLoader.loadTemplateStub = { _ in
|
||||
ProjectDescription.Template(
|
||||
description: "desc",
|
||||
files: [ProjectDescription.Template.File(
|
||||
items: [ProjectDescription.Template.Item(
|
||||
path: "generateOne",
|
||||
contents: .file("fileOne")
|
||||
)]
|
||||
|
@ -58,7 +58,7 @@ final class TemplateLoaderTests: TuistUnitTestCase {
|
|||
// Then
|
||||
XCTAssertEqual(got, TuistGraph.Template(
|
||||
description: "desc",
|
||||
files: [Template.File(
|
||||
items: [Template.Item(
|
||||
path: RelativePath("generateOne"),
|
||||
contents: .file(temporaryPath.appending(component: "fileOne"))
|
||||
)]
|
||||
|
|
|
@ -27,11 +27,11 @@ final class TemplateGeneratorTests: TuistTestCase {
|
|||
let directories = [RelativePath("a"), RelativePath("a/b"), RelativePath("c")]
|
||||
let destinationPath = try temporaryPath()
|
||||
let expectedDirectories = directories.map(destinationPath.appending)
|
||||
let files = directories.map {
|
||||
Template.File.test(path: RelativePath($0.pathString + "/file.swift"))
|
||||
let items = directories.map {
|
||||
Template.Item.test(path: RelativePath($0.pathString + "/file.swift"))
|
||||
}
|
||||
|
||||
let template = Template.test(files: files)
|
||||
let template = Template.test(items: items)
|
||||
|
||||
// When
|
||||
try subject.generate(
|
||||
|
@ -47,10 +47,10 @@ final class TemplateGeneratorTests: TuistTestCase {
|
|||
func test_directories_with_attributes() throws {
|
||||
// Given
|
||||
let directories = [RelativePath("{{ name }}"), RelativePath("{{ aName }}"), RelativePath("{{ name }}/{{ bName }}")]
|
||||
let files = directories.map {
|
||||
Template.File.test(path: RelativePath($0.pathString + "/file.swift"))
|
||||
let items = directories.map {
|
||||
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 expectedDirectories = [RelativePath("test_name"),
|
||||
RelativePath("test"),
|
||||
|
@ -71,19 +71,19 @@ final class TemplateGeneratorTests: TuistTestCase {
|
|||
|
||||
func test_files_are_generated() throws {
|
||||
// Given
|
||||
let files: [Template.File] = [
|
||||
Template.File(path: RelativePath("a"), contents: .string("aContent")),
|
||||
Template.File(path: RelativePath("b"), contents: .string("bContent")),
|
||||
let items: [Template.Item] = [
|
||||
Template.Item(path: RelativePath("a"), contents: .string("aContent")),
|
||||
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 expectedFiles: [(AbsolutePath, String)] = files.compactMap {
|
||||
let expectedFiles: [(AbsolutePath, String)] = items.compactMap {
|
||||
let content: String
|
||||
switch $0.contents {
|
||||
case let .string(staticContent):
|
||||
content = staticContent
|
||||
case .file:
|
||||
case .file, .directory:
|
||||
XCTFail("Unexpected type")
|
||||
return nil
|
||||
}
|
||||
|
@ -106,12 +106,12 @@ final class TemplateGeneratorTests: TuistTestCase {
|
|||
func test_files_are_generated_with_attributes() throws {
|
||||
// Given
|
||||
let sourcePath = try temporaryPath()
|
||||
let files = [
|
||||
Template.File(path: RelativePath("{{ name }}"), contents: .string("{{ contentName }}")),
|
||||
Template.File(path: RelativePath("{{ directoryName }}/{{ fileName }}"), contents: .string("bContent")),
|
||||
Template.File(path: RelativePath("file"), contents: .file(sourcePath.appending(component: "{{ filePath }}"))),
|
||||
let items = [
|
||||
Template.Item(path: RelativePath("{{ name }}"), contents: .string("{{ contentName }}")),
|
||||
Template.Item(path: RelativePath("{{ directoryName }}/{{ fileName }}"), contents: .string("bContent")),
|
||||
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 contentName = "test content"
|
||||
let fileContent = "test file content"
|
||||
|
@ -152,11 +152,11 @@ final class TemplateGeneratorTests: TuistTestCase {
|
|||
let name = "test name"
|
||||
let aContent = "test a content"
|
||||
let bContent = "test b content"
|
||||
let files = [
|
||||
Template.File(path: RelativePath("a/file"), contents: .file(sourcePath.appending(component: "testFile"))),
|
||||
Template.File(path: RelativePath("b/{{ name }}/file"), contents: .file(sourcePath.appending(components: "bTestFile"))),
|
||||
let items = [
|
||||
Template.Item(path: RelativePath("a/file"), contents: .file(sourcePath.appending(component: "testFile"))),
|
||||
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(bContent, path: sourcePath.appending(component: "bTestFile"), atomically: true)
|
||||
let expectedFiles: [(AbsolutePath, String)] = [
|
||||
|
@ -187,7 +187,7 @@ final class TemplateGeneratorTests: TuistTestCase {
|
|||
path: sourcePath.appending(component: "a.stencil"),
|
||||
atomically: true
|
||||
)
|
||||
let template = Template.test(files: [Template.File(
|
||||
let template = Template.test(items: [Template.Item(
|
||||
path: RelativePath("a"),
|
||||
contents: .file(sourcePath.appending(component: "a.stencil"))
|
||||
)])
|
||||
|
@ -222,12 +222,12 @@ final class TemplateGeneratorTests: TuistTestCase {
|
|||
path: sourcePath.appending(component: "a.swift"),
|
||||
atomically: true
|
||||
)
|
||||
let template = Template.test(files: [
|
||||
Template.File(
|
||||
let template = Template.test(items: [
|
||||
Template.Item(
|
||||
path: RelativePath("unrendered"),
|
||||
contents: .file(sourcePath.appending(component: "a.swift"))
|
||||
),
|
||||
Template.File(
|
||||
Template.Item(
|
||||
path: RelativePath("rendered"),
|
||||
contents: .file(sourcePath.appending(component: "a.stencil"))
|
||||
),
|
||||
|
@ -260,8 +260,8 @@ final class TemplateGeneratorTests: TuistTestCase {
|
|||
path: sourcePath.appending(component: "b.stencil"),
|
||||
atomically: true
|
||||
)
|
||||
let template = Template.test(files: [
|
||||
Template.File(
|
||||
let template = Template.test(items: [
|
||||
Template.Item(
|
||||
path: RelativePath("ignore"),
|
||||
contents: .file(sourcePath.appending(component: "b.stencil"))
|
||||
),
|
||||
|
@ -277,4 +277,39 @@ final class TemplateGeneratorTests: TuistTestCase {
|
|||
// Then
|
||||
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",
|
||||
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)
|
||||
|
||||
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.
|
||||
|
|
|
@ -25,6 +25,23 @@ Feature: Scaffold a project using Tuist
|
|||
"""
|
||||
// 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)
|
||||
|
|
|
@ -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