mirror of
https://github.com/laosb/SwiftTailwind.git
synced 2025-11-28 22:01:38 +00:00
First working version.
This commit is contained in:
parent
2f70b79493
commit
8fbeb20900
6 changed files with 227 additions and 2 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
// swift-tools-version: 5.6
|
// swift-tools-version: 6.1
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
|
||||||
|
|
@ -9,6 +9,18 @@ let package = Package(
|
||||||
.plugin(name: "TailwindCSS", targets: ["TailwindCSS"])
|
.plugin(name: "TailwindCSS", targets: ["TailwindCSS"])
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.plugin(name: "TailwindCSS", capability: .buildTool())
|
.plugin(name: "TailwindCSS", capability: .buildTool(), dependencies: ["TailwindCSSCLI"]),
|
||||||
|
.binaryTarget(
|
||||||
|
name: "TailwindCSSCLI",
|
||||||
|
url:
|
||||||
|
"https://github.com/laosb/SwiftTailwind/releases/download/4.1.12-test-manual.1/tailwindcss.artifactbundle.zip",
|
||||||
|
checksum: "bfa96ef1d4d1b665bb40c89ec906044c9532b3cabf866fbe2bd3e5a95bf40bea"
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "SwiftTailwindExample",
|
||||||
|
resources: [.copy("Folder")],
|
||||||
|
plugins: ["TailwindCSS"]
|
||||||
|
),
|
||||||
|
.testTarget(name: "SwiftTailwindTests", dependencies: ["SwiftTailwindExample"]),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
117
Plugins/TailwindCSS/TailwindCSSBuildPlugin.swift
Normal file
117
Plugins/TailwindCSS/TailwindCSSBuildPlugin.swift
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
import Foundation
|
||||||
|
import PackagePlugin
|
||||||
|
import RegexBuilder
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct TailwindCSSBuildPlugin: BuildToolPlugin {
|
||||||
|
let inputCSSFilename = "Tailwind.css"
|
||||||
|
let outputBundleName = "TailwindCSS.bundle"
|
||||||
|
let outputCSSFilename = "tw.css"
|
||||||
|
|
||||||
|
let importStatementRegex = Regex {
|
||||||
|
Anchor.startOfLine
|
||||||
|
ZeroOrMore(.whitespace)
|
||||||
|
"@import"
|
||||||
|
ZeroOrMore(.whitespace)
|
||||||
|
"\"tailwindcss\""
|
||||||
|
ZeroOrMore(.whitespace)
|
||||||
|
"source("
|
||||||
|
ZeroOrMore(.whitespace)
|
||||||
|
"none"
|
||||||
|
ZeroOrMore(.whitespace)
|
||||||
|
")"
|
||||||
|
ZeroOrMore(.whitespace)
|
||||||
|
";"
|
||||||
|
}.anchorsMatchLineEndings()
|
||||||
|
|
||||||
|
let sourceDeclarationRegex = Regex {
|
||||||
|
Anchor.startOfLine
|
||||||
|
ZeroOrMore(.whitespace)
|
||||||
|
"@source"
|
||||||
|
ZeroOrMore(.whitespace)
|
||||||
|
"\""
|
||||||
|
Capture(ZeroOrMore(.word))
|
||||||
|
"\""
|
||||||
|
ZeroOrMore(.whitespace)
|
||||||
|
";"
|
||||||
|
}
|
||||||
|
|
||||||
|
let sourceNotDeclarationRegex = Regex {
|
||||||
|
Anchor.startOfLine
|
||||||
|
ZeroOrMore(.whitespace)
|
||||||
|
"@source"
|
||||||
|
ZeroOrMore(.whitespace)
|
||||||
|
"not"
|
||||||
|
}.anchorsMatchLineEndings()
|
||||||
|
|
||||||
|
func createBuildCommands(
|
||||||
|
context: PluginContext,
|
||||||
|
target: Target
|
||||||
|
) throws -> [Command] {
|
||||||
|
let tailwindCSSURL: URL = target.directoryURL.appending(component: "Tailwind.css")
|
||||||
|
guard let cssContent = try? String(contentsOf: tailwindCSSURL) else {
|
||||||
|
throw BuildError.missingTailwindCSSFile
|
||||||
|
}
|
||||||
|
let matches = cssContent.matches(of: importStatementRegex)
|
||||||
|
guard !matches.isEmpty else {
|
||||||
|
throw BuildError.missingImportStatement
|
||||||
|
}
|
||||||
|
if (try? sourceNotDeclarationRegex.firstMatch(in: cssContent)) != nil {
|
||||||
|
throw BuildError.sourceNotDeclarationUnsupported
|
||||||
|
}
|
||||||
|
|
||||||
|
let sourcePaths =
|
||||||
|
cssContent
|
||||||
|
.matches(of: sourceDeclarationRegex)
|
||||||
|
.compactMap { String($0.output.1) }
|
||||||
|
let sourceURLs: [URL] = sourcePaths.map { path in
|
||||||
|
target.directoryURL
|
||||||
|
.appending(component: path, directoryHint: .inferFromPath)
|
||||||
|
.resolvingSymlinksInPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
let tailwindCLIURL: URL = try context.tool(named: "tailwindcss").url
|
||||||
|
let outputBundleURL = context.pluginWorkDirectoryURL
|
||||||
|
.appending(component: outputBundleName, directoryHint: .isDirectory)
|
||||||
|
let outputURL = outputBundleURL.appending(
|
||||||
|
component: outputCSSFilename, directoryHint: .notDirectory)
|
||||||
|
|
||||||
|
print("Tailwind CSS Build Plugin: \(tailwindCSSURL.path)")
|
||||||
|
print("Tailwind CSS File: \(tailwindCSSURL.path)")
|
||||||
|
print("Source files: \n -\(sourceURLs.map(\.path).joined(separator: "\n -"))")
|
||||||
|
print("Output: \(outputURL.path)")
|
||||||
|
|
||||||
|
return [
|
||||||
|
.buildCommand(
|
||||||
|
displayName: "Building Tailwind CSS",
|
||||||
|
executable: tailwindCLIURL,
|
||||||
|
arguments: [
|
||||||
|
"--input", tailwindCSSURL.path,
|
||||||
|
"--output", outputURL.path,
|
||||||
|
"--minify",
|
||||||
|
],
|
||||||
|
inputFiles: [tailwindCSSURL] + sourceURLs,
|
||||||
|
outputFiles: [outputBundleURL]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TailwindCSSBuildPlugin {
|
||||||
|
enum BuildError: Error {
|
||||||
|
case missingTailwindCSSFile
|
||||||
|
case missingImportStatement
|
||||||
|
case sourceNotDeclarationUnsupported
|
||||||
|
|
||||||
|
var localizedDescription: String {
|
||||||
|
switch self {
|
||||||
|
case .missingTailwindCSSFile:
|
||||||
|
"Tailwind.css file not found in the target directory."
|
||||||
|
case .missingImportStatement:
|
||||||
|
"No `@import \"tailwind\"` statement found, or `source(none)` is missing."
|
||||||
|
case .sourceNotDeclarationUnsupported:
|
||||||
|
"`@source not` declarations are not supported. Please explicitly declare sources with `@source \"<path>\";`."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
57
Sources/SwiftTailwindExample/Example.swift
Normal file
57
Sources/SwiftTailwindExample/Example.swift
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct Example {
|
||||||
|
public var htmlInSwiftCode =
|
||||||
|
"""
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Example</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1 class="text-2xl">Hello, World!</h1>
|
||||||
|
<p>This is an example of HTML in Swift code.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
public func getTestHTML() throws -> String? {
|
||||||
|
guard let fileURL = Bundle.module.path(forResource: "Test", ofType: "html") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return try String(contentsOfFile: fileURL, encoding: .utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getGeneratedCSS() throws -> String? {
|
||||||
|
guard
|
||||||
|
let tailwindCSSBundleURL = Bundle.module.url(
|
||||||
|
forResource: "TailwindCSS", withExtension: "bundle")
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let cssFileURL = tailwindCSSBundleURL.appendingPathComponent("tw.css")
|
||||||
|
return try String(contentsOf: cssFileURL, encoding: .utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func printAll() {
|
||||||
|
print("HTML in Swift Code:")
|
||||||
|
print(htmlInSwiftCode)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("Test.html:")
|
||||||
|
if let testHTML = try! getTestHTML() {
|
||||||
|
print(testHTML)
|
||||||
|
} else {
|
||||||
|
print("Test.html not found.")
|
||||||
|
}
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("Output Tailwind CSS (minified):")
|
||||||
|
if let generatedCSS = try! getGeneratedCSS() {
|
||||||
|
print(generatedCSS)
|
||||||
|
} else {
|
||||||
|
print("Tailwind CSS not found.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Sources/SwiftTailwindExample/Folder/Test.html
Normal file
7
Sources/SwiftTailwindExample/Folder/Test.html
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<html>
|
||||||
|
<!-- Generated CSS will be avilable in tw.css file in a TailwindCSS.bundle that can be accessed under `Bundle.module`. -->
|
||||||
|
<link rel="stylesheet" href="/TailwindCSS.bundle/tw.css" />
|
||||||
|
<body>
|
||||||
|
<p class="text-[#f05138] font-bold">Hello, Swift!</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
7
Sources/SwiftTailwindExample/Tailwind.css
Normal file
7
Sources/SwiftTailwindExample/Tailwind.css
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
/** SwiftTailwind assumes a Tailwind.css file is present in the root of target directory. **/
|
||||||
|
|
||||||
|
/** To align with Swift Package Manager's build plugin capability, SwiftTailwind requires explicitly register all inputs. */
|
||||||
|
/** You must specify `source(none)` after `tailwindcss` import and add `@source` for each source file / folder you want to include. */
|
||||||
|
@import "tailwindcss" source(none);
|
||||||
|
@source "./Example.swift";
|
||||||
|
@source "./Folder";
|
||||||
25
Tests/SwiftTailwindTests/Tests.swift
Normal file
25
Tests/SwiftTailwindTests/Tests.swift
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import Testing
|
||||||
|
|
||||||
|
@testable import SwiftTailwindExample
|
||||||
|
|
||||||
|
@Suite("SwiftTailwindExample")
|
||||||
|
struct SwiftTailwindExampleTests {
|
||||||
|
@Test
|
||||||
|
func example() throws {
|
||||||
|
let example = Example()
|
||||||
|
let generatedCSS = try example.getGeneratedCSS()
|
||||||
|
#expect(generatedCSS != nil)
|
||||||
|
#expect(
|
||||||
|
generatedCSS?.contains("text-2xl") == true,
|
||||||
|
"Class used in Swift code is generated."
|
||||||
|
)
|
||||||
|
#expect(
|
||||||
|
generatedCSS?.contains("text-\\[\\#f05138\\]") == true,
|
||||||
|
"Arbitary value class used in Test.html is generated."
|
||||||
|
)
|
||||||
|
#expect(
|
||||||
|
generatedCSS?.contains("bg-blue-500") == false,
|
||||||
|
"Class not used is not generated."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue