mirror of
https://github.com/laosb/SwiftTailwind.git
synced 2025-11-29 06:11:38 +00:00
Compare commits
No commits in common. "main" and "1.0.0+tw.4.1.12" have entirely different histories.
main
...
1.0.0+tw.4
21 changed files with 121 additions and 666 deletions
65
.github/workflows/ci.yml
vendored
65
.github/workflows/ci.yml
vendored
|
|
@ -1,65 +0,0 @@
|
||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test-linux:
|
|
||||||
name: Test on Linux
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
swift-version: ["6.1", "6.2"]
|
|
||||||
container:
|
|
||||||
image: swift:${{ matrix.swift-version }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Cache Swift packages
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: .build
|
|
||||||
key: ${{ runner.os }}-spm-${{ matrix.swift-version }}-${{ hashFiles('**/Package.resolved') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-spm-${{ matrix.swift-version }}-
|
|
||||||
${{ runner.os }}-spm-
|
|
||||||
|
|
||||||
- name: Build package
|
|
||||||
run: swift build --verbose
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: swift test --verbose
|
|
||||||
|
|
||||||
test-macos:
|
|
||||||
name: Test on macOS
|
|
||||||
runs-on: macos-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
swift-version: ["6.1", "6.2"]
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Swift ${{ matrix.swift-version }}
|
|
||||||
uses: swift-actions/setup-swift@v2
|
|
||||||
with:
|
|
||||||
swift-version: ${{ matrix.swift-version }}
|
|
||||||
|
|
||||||
- name: Cache Swift packages
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: .build
|
|
||||||
key: ${{ runner.os }}-spm-${{ matrix.swift-version }}-${{ hashFiles('**/Package.resolved') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-spm-${{ matrix.swift-version }}-
|
|
||||||
${{ runner.os }}-spm-
|
|
||||||
|
|
||||||
- name: Build package
|
|
||||||
run: swift build --verbose
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: swift test --verbose
|
|
||||||
104
.github/workflows/release-tailwindcss-cli.yml
vendored
104
.github/workflows/release-tailwindcss-cli.yml
vendored
|
|
@ -1,104 +0,0 @@
|
||||||
name: Release TailwindCSS CLI Artifacts
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
tailwind_version:
|
|
||||||
description: "TailwindCSS release version (e.g., v4.1.14)"
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-release:
|
|
||||||
name: Build and Release Artifacts
|
|
||||||
runs-on: macos-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Swift
|
|
||||||
uses: swift-actions/setup-swift@v2
|
|
||||||
with:
|
|
||||||
swift-version: "6.1"
|
|
||||||
|
|
||||||
- name: Cache Swift packages
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: .build
|
|
||||||
key: ${{ runner.os }}-spm-release-${{ hashFiles('**/Package.resolved') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-spm-release-
|
|
||||||
${{ runner.os }}-spm-
|
|
||||||
|
|
||||||
- name: Validate version format
|
|
||||||
run: |
|
|
||||||
if [[ ! "${{ inputs.tailwind_version }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
||||||
echo "Error: Version must be in format vX.Y.Z (e.g., v4.1.14)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "Version format is valid: ${{ inputs.tailwind_version }}"
|
|
||||||
|
|
||||||
- name: Build artifact bundler
|
|
||||||
run: |
|
|
||||||
echo "Building TailwindCSSCLIArtifactBundler..."
|
|
||||||
swift build -c release --product TailwindCSSCLIArtifactBundler
|
|
||||||
|
|
||||||
- name: Create artifact bundles
|
|
||||||
run: |
|
|
||||||
echo "Creating artifact bundles for TailwindCSS ${{ inputs.tailwind_version }}..."
|
|
||||||
mkdir -p ./artifacts
|
|
||||||
.build/release/TailwindCSSCLIArtifactBundler \
|
|
||||||
--version "${{ inputs.tailwind_version }}" \
|
|
||||||
--work-dir "${{ runner.temp }}/tailwindcss-bundles" \
|
|
||||||
--output-dir ./artifacts
|
|
||||||
|
|
||||||
- name: List generated artifacts
|
|
||||||
run: |
|
|
||||||
echo "Generated artifacts:"
|
|
||||||
ls -lh artifacts/
|
|
||||||
|
|
||||||
- name: Compute checksum
|
|
||||||
id: checksum
|
|
||||||
run: |
|
|
||||||
CHECKSUM=$(shasum -a 256 artifacts/tailwindcss.artifactbundleindex | awk '{print $1}')
|
|
||||||
echo "checksum=$CHECKSUM" >> $GITHUB_OUTPUT
|
|
||||||
echo "Artifact bundle index checksum: $CHECKSUM"
|
|
||||||
|
|
||||||
- name: Create release
|
|
||||||
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
|
||||||
with:
|
|
||||||
tag_name: TailwindCSSCLI-${{ inputs.tailwind_version }}
|
|
||||||
name: TailwindCSS CLI ${{ inputs.tailwind_version }}
|
|
||||||
body: |
|
|
||||||
## TailwindCSS CLI Artifact Bundles
|
|
||||||
|
|
||||||
This release contains Swift Package Manager artifact bundles for TailwindCSS CLI version `${{ inputs.tailwind_version }}`.
|
|
||||||
|
|
||||||
### Included Artifacts
|
|
||||||
- Linux x64 (`x86_64-unknown-linux-gnu`)
|
|
||||||
- macOS x64 (`x86_64-apple-darwin`)
|
|
||||||
- macOS ARM64 (`aarch64-apple-darwin`)
|
|
||||||
|
|
||||||
### Usage
|
|
||||||
Add the artifact bundle to your `Package.swift`:
|
|
||||||
```swift
|
|
||||||
.binaryTarget(
|
|
||||||
name: "TailwindCSSCLI",
|
|
||||||
url: "https://github.com/${{ github.repository }}/releases/download/TailwindCSSCLI@${{ inputs.tailwind_version }}/tailwindcss.artifactbundleindex",
|
|
||||||
checksum: "${{ steps.checksum.outputs.checksum }}"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Checksum
|
|
||||||
```
|
|
||||||
${{ steps.checksum.outputs.checksum }}
|
|
||||||
```
|
|
||||||
draft: false
|
|
||||||
prerelease: false
|
|
||||||
make_latest: false
|
|
||||||
files: |
|
|
||||||
artifacts/*.zip
|
|
||||||
artifacts/tailwindcss.artifactbundleindex
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
// Folder-specific settings
|
|
||||||
//
|
|
||||||
// For a full list of overridable settings, and general information on folder-specific settings,
|
|
||||||
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
|
|
||||||
{
|
|
||||||
"languages": {
|
|
||||||
"CSS": {
|
|
||||||
"language_servers": ["!vscode-css-language-server"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
{
|
|
||||||
"originHash" : "8e39da950f9cbe4c8126b1fabf0d3d6945ab547f7564fd7bb0921701f3269c14",
|
|
||||||
"pins" : [
|
|
||||||
{
|
|
||||||
"identity" : "swift-argument-parser",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/apple/swift-argument-parser.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "309a47b2b1d9b5e991f36961c983ecec72275be3",
|
|
||||||
"version" : "1.6.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-asn1",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/apple/swift-asn1.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "f70225981241859eb4aa1a18a75531d26637c8cc",
|
|
||||||
"version" : "1.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-crypto",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/apple/swift-crypto.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "95ba0316a9b733e92bb6b071255ff46263bbe7dc",
|
|
||||||
"version" : "3.15.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"version" : 3
|
|
||||||
}
|
|
||||||
|
|
@ -6,31 +6,19 @@ let package = Package(
|
||||||
name: "SwiftTailwind",
|
name: "SwiftTailwind",
|
||||||
platforms: [.macOS(.v12)],
|
platforms: [.macOS(.v12)],
|
||||||
products: [
|
products: [
|
||||||
.plugin(name: "TailwindCSS", targets: ["TailwindCSS"]),
|
.plugin(name: "TailwindCSS", targets: ["TailwindCSS"])
|
||||||
.executable(name: "TailwindCSSCLIArtifactBundler", targets: ["TailwindCSSCLIArtifactBundler"]),
|
|
||||||
],
|
|
||||||
dependencies: [
|
|
||||||
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"),
|
|
||||||
.package(url: "https://github.com/apple/swift-crypto.git", from: "3.0.0"),
|
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.executableTarget(
|
|
||||||
name: "TailwindCSSCLIArtifactBundler",
|
|
||||||
dependencies: [
|
|
||||||
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
|
||||||
.product(name: "Crypto", package: "swift-crypto"),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
.plugin(name: "TailwindCSS", capability: .buildTool(), dependencies: ["TailwindCSSCLI"]),
|
.plugin(name: "TailwindCSS", capability: .buildTool(), dependencies: ["TailwindCSSCLI"]),
|
||||||
.binaryTarget(
|
.binaryTarget(
|
||||||
name: "TailwindCSSCLI",
|
name: "TailwindCSSCLI",
|
||||||
url:
|
url:
|
||||||
"https://github.com/laosb/SwiftTailwind/releases/download/1.1.0-test.4+tw.4.1.14/tailwindcss.artifactbundleindex",
|
"https://github.com/laosb/SwiftTailwind/releases/download/1.0.0+tw.4.1.12/tailwindcss.artifactbundle.zip",
|
||||||
checksum: "ec4df49e361db5fc3159c431e5661af2e1b22d8575c64ce0482a8e84cfd93d04"
|
checksum: "bfa96ef1d4d1b665bb40c89ec906044c9532b3cabf866fbe2bd3e5a95bf40bea"
|
||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "SwiftTailwindExample",
|
name: "SwiftTailwindExample",
|
||||||
resources: [.copy("Views/Test.html")],
|
resources: [.copy("Folder")],
|
||||||
plugins: ["TailwindCSS"]
|
plugins: ["TailwindCSS"]
|
||||||
),
|
),
|
||||||
.testTarget(name: "SwiftTailwindTests", dependencies: ["SwiftTailwindExample"]),
|
.testTarget(name: "SwiftTailwindTests", dependencies: ["SwiftTailwindExample"]),
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ struct TailwindCSSBuildPlugin: BuildToolPlugin {
|
||||||
"@source"
|
"@source"
|
||||||
ZeroOrMore(.whitespace)
|
ZeroOrMore(.whitespace)
|
||||||
"\""
|
"\""
|
||||||
Capture { OneOrMore(CharacterClass.anyOf("\"").inverted) }
|
Capture(ZeroOrMore(.word))
|
||||||
"\""
|
"\""
|
||||||
ZeroOrMore(.whitespace)
|
ZeroOrMore(.whitespace)
|
||||||
";"
|
";"
|
||||||
|
|
@ -48,12 +48,8 @@ struct TailwindCSSBuildPlugin: BuildToolPlugin {
|
||||||
context: PluginContext,
|
context: PluginContext,
|
||||||
target: Target
|
target: Target
|
||||||
) throws -> [Command] {
|
) throws -> [Command] {
|
||||||
guard let sourceFileURLs = target.sourceModule?.sourceFiles.map({ $0.url }) else {
|
|
||||||
throw BuildError.notASourceModule
|
|
||||||
}
|
|
||||||
|
|
||||||
let tailwindCSSURL: URL = target.directoryURL.appending(component: "Tailwind.css")
|
let tailwindCSSURL: URL = target.directoryURL.appending(component: "Tailwind.css")
|
||||||
guard let cssContent = try? String(contentsOf: tailwindCSSURL, encoding: .utf8) else {
|
guard let cssContent = try? String(contentsOf: tailwindCSSURL) else {
|
||||||
throw BuildError.missingTailwindCSSFile
|
throw BuildError.missingTailwindCSSFile
|
||||||
}
|
}
|
||||||
let matches = cssContent.matches(of: importStatementRegex)
|
let matches = cssContent.matches(of: importStatementRegex)
|
||||||
|
|
@ -64,36 +60,27 @@ struct TailwindCSSBuildPlugin: BuildToolPlugin {
|
||||||
throw BuildError.sourceNotDeclarationUnsupported
|
throw BuildError.sourceNotDeclarationUnsupported
|
||||||
}
|
}
|
||||||
|
|
||||||
let sourcePatterns =
|
let sourcePaths =
|
||||||
cssContent
|
cssContent
|
||||||
.matches(of: sourceDeclarationRegex)
|
.matches(of: sourceDeclarationRegex)
|
||||||
.compactMap { String($0.output.1) }
|
.compactMap { String($0.output.1) }
|
||||||
let sourcePatternURLs: [URL] = sourcePatterns.map { path in
|
let sourceURLs: [URL] = sourcePaths.map { path in
|
||||||
// Simplified handling: If `**` is used, we just include everything in the directory.
|
// Simplified handling: If ** is used, we just include everything in the directory.
|
||||||
// It's unlikely we will have the same glob processing logic as Tailwind CSS CLI,
|
|
||||||
// so we may as well just expand the coverage.
|
|
||||||
// This only affects SwiftPM change detection: Tailwind CSS CLI will handle the globbing correctly.
|
|
||||||
let globlessPath = path.replacing(/\*\*.*/, with: "")
|
let globlessPath = path.replacing(/\*\*.*/, with: "")
|
||||||
return target.directoryURL
|
return target.directoryURL
|
||||||
.appending(component: globlessPath, directoryHint: .inferFromPath)
|
.appending(component: globlessPath, directoryHint: .inferFromPath)
|
||||||
.resolvingSymlinksInPath()
|
.resolvingSymlinksInPath()
|
||||||
}
|
}
|
||||||
|
|
||||||
let includedSourceURLs = sourceFileURLs.filter { file in
|
|
||||||
sourcePatternURLs.contains { file.isOrIsDescendant(of: $0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
let tailwindCLIURL: URL = try context.tool(named: "tailwindcss").url
|
let tailwindCLIURL: URL = try context.tool(named: "tailwindcss").url
|
||||||
let outputBundleURL = context.pluginWorkDirectoryURL
|
let outputBundleURL = context.pluginWorkDirectoryURL
|
||||||
.appending(component: outputBundleName, directoryHint: .isDirectory)
|
.appending(component: outputBundleName, directoryHint: .isDirectory)
|
||||||
let outputURL = outputBundleURL.appending(
|
let outputURL = outputBundleURL.appending(
|
||||||
component: outputCSSFilename, directoryHint: .notDirectory)
|
component: outputCSSFilename, directoryHint: .notDirectory)
|
||||||
|
|
||||||
print("Tailwind CSS Build Plugin")
|
print("Tailwind CSS Build Plugin: \(tailwindCSSURL.path)")
|
||||||
print("Tailwind.css: \(tailwindCSSURL.path)")
|
print("Tailwind CSS File: \(tailwindCSSURL.path)")
|
||||||
print("@source declarations: \(sourcePatterns)")
|
print("Source files: \n -\(sourceURLs.map(\.path).joined(separator: "\n -"))")
|
||||||
print("All source files: \(sourceFileURLs.map(\.path))")
|
|
||||||
print("Input files: \(includedSourceURLs.map(\.path))")
|
|
||||||
print("Output: \(outputURL.path)")
|
print("Output: \(outputURL.path)")
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
@ -105,7 +92,7 @@ struct TailwindCSSBuildPlugin: BuildToolPlugin {
|
||||||
"--output", outputURL.path,
|
"--output", outputURL.path,
|
||||||
"--minify",
|
"--minify",
|
||||||
],
|
],
|
||||||
inputFiles: [tailwindCSSURL] + includedSourceURLs,
|
inputFiles: [tailwindCSSURL] + sourceURLs,
|
||||||
outputFiles: [outputBundleURL]
|
outputFiles: [outputBundleURL]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
@ -114,15 +101,12 @@ struct TailwindCSSBuildPlugin: BuildToolPlugin {
|
||||||
|
|
||||||
extension TailwindCSSBuildPlugin {
|
extension TailwindCSSBuildPlugin {
|
||||||
enum BuildError: Error {
|
enum BuildError: Error {
|
||||||
case notASourceModule
|
|
||||||
case missingTailwindCSSFile
|
case missingTailwindCSSFile
|
||||||
case missingImportStatement
|
case missingImportStatement
|
||||||
case sourceNotDeclarationUnsupported
|
case sourceNotDeclarationUnsupported
|
||||||
|
|
||||||
var localizedDescription: String {
|
var localizedDescription: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .notASourceModule:
|
|
||||||
"The target is not a source module."
|
|
||||||
case .missingTailwindCSSFile:
|
case .missingTailwindCSSFile:
|
||||||
"Tailwind.css file not found in the target directory."
|
"Tailwind.css file not found in the target directory."
|
||||||
case .missingImportStatement:
|
case .missingImportStatement:
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import Foundation
|
|
||||||
|
|
||||||
extension URL {
|
|
||||||
func isOrIsDescendant(of ancestor: URL) -> Bool {
|
|
||||||
guard ancestor.isFileURL, self.isFileURL else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
let ancestorComponents = ancestor.pathComponents
|
|
||||||
let selfComponents = self.pathComponents
|
|
||||||
|
|
||||||
guard selfComponents.count >= ancestorComponents.count else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for (index, component) in ancestorComponents.enumerated() {
|
|
||||||
if selfComponents[index] != component {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
83
Scripts/buildArtifactBundle.sh
Executable file
83
Scripts/buildArtifactBundle.sh
Executable file
|
|
@ -0,0 +1,83 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Check if TAILWINDCSS_VERSION environment variable is set
|
||||||
|
if [[ -z "${TAILWINDCSS_VERSION:-}" ]]; then
|
||||||
|
echo "Error: TAILWINDCSS_VERSION environment variable is not set"
|
||||||
|
echo "Usage: TAILWINDCSS_VERSION=v3.4.0 $0"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION="$TAILWINDCSS_VERSION"
|
||||||
|
echo "Building artifact bundle for TailwindCSS version: $VERSION"
|
||||||
|
|
||||||
|
# Get the directory containing this script to find the template
|
||||||
|
TEMPLATE_FILE="$PWD/Scripts/info.template.json"
|
||||||
|
|
||||||
|
# Check if template file exists
|
||||||
|
if [[ ! -f "$TEMPLATE_FILE" ]]; then
|
||||||
|
echo "Error: Template file not found at $TEMPLATE_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create working directory
|
||||||
|
WORK_DIR="/tmp/tailwindcss.artifactbundle"
|
||||||
|
echo "Creating working directory: $WORK_DIR"
|
||||||
|
rm -rf "$WORK_DIR"
|
||||||
|
mkdir -p "$WORK_DIR"
|
||||||
|
cd "$WORK_DIR"
|
||||||
|
|
||||||
|
# GitHub release base URL
|
||||||
|
BASE_URL="https://github.com/tailwindlabs/tailwindcss/releases/download/$VERSION"
|
||||||
|
|
||||||
|
# Download and place binaries
|
||||||
|
download_binary() {
|
||||||
|
local binary_name="$1"
|
||||||
|
local target_path="$2"
|
||||||
|
local target_dir
|
||||||
|
target_dir=$(dirname "$target_path")
|
||||||
|
|
||||||
|
echo "Downloading $binary_name..."
|
||||||
|
mkdir -p "$target_dir"
|
||||||
|
|
||||||
|
if curl -L "$BASE_URL/$binary_name" > "$target_path"; then
|
||||||
|
chmod +x "$target_path"
|
||||||
|
echo "✓ Downloaded and made executable: $target_path"
|
||||||
|
else
|
||||||
|
echo "✗ Failed to download $binary_name from $BASE_URL/$binary_name"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Download each binary to its target location
|
||||||
|
download_binary "tailwindcss-linux-x64" "tailwindcss-$VERSION-linux-x64/bin/tailwindcss"
|
||||||
|
download_binary "tailwindcss-macos-x64" "tailwindcss-$VERSION-macos-x64/bin/tailwindcss"
|
||||||
|
download_binary "tailwindcss-macos-arm64" "tailwindcss-$VERSION-macos-arm64/bin/tailwindcss"
|
||||||
|
|
||||||
|
# Create info.json from template, replacing %VERSION% with actual version
|
||||||
|
echo "Creating info.json from template..."
|
||||||
|
sed "s/%VERSION%/$VERSION/g" "$TEMPLATE_FILE" > "info.json"
|
||||||
|
echo "✓ Created info.json with version $VERSION"
|
||||||
|
|
||||||
|
# Create ZIP file
|
||||||
|
ZIP_FILE="/tmp/tailwindcss.artifactbundle.zip"
|
||||||
|
echo "Creating ZIP file: $ZIP_FILE"
|
||||||
|
|
||||||
|
# Remove existing ZIP file if it exists
|
||||||
|
rm -f "$ZIP_FILE"
|
||||||
|
|
||||||
|
# Create ZIP with the artifact bundle as the only child in root
|
||||||
|
cd /tmp
|
||||||
|
zip -r "tailwindcss.artifactbundle.zip" "tailwindcss.artifactbundle"
|
||||||
|
|
||||||
|
echo "✓ Created ZIP file: $ZIP_FILE"
|
||||||
|
|
||||||
|
# Compute checksum using Swift Package Manager
|
||||||
|
echo "Computing checksum..."
|
||||||
|
CHECKSUM=$(swift package compute-checksum "$ZIP_FILE")
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== BUILD COMPLETE ==="
|
||||||
|
echo "ZIP file path: $ZIP_FILE"
|
||||||
|
echo "Checksum: $CHECKSUM"
|
||||||
23
Scripts/info.template.json
Normal file
23
Scripts/info.template.json
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"schemaVersion": "1.0",
|
||||||
|
"artifacts": {
|
||||||
|
"tailwindcss": {
|
||||||
|
"version": "%VERSION%",
|
||||||
|
"type": "executable",
|
||||||
|
"variants": [
|
||||||
|
{
|
||||||
|
"path": "tailwindcss-%VERSION%-macos-x64/bin/tailwindcss",
|
||||||
|
"supportedTriples": ["x86_64-apple-macosx"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tailwindcss-%VERSION%-macos-arm64/bin/tailwindcss",
|
||||||
|
"supportedTriples": ["arm64-apple-macosx"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tailwindcss-%VERSION%-linux-x64/bin/tailwindcss",
|
||||||
|
"supportedTriples": ["x86_64-unknown-linux-gnu"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
// This file is not included in Tailwind.css, so TW classes here will not be generated.
|
|
||||||
let html =
|
|
||||||
"""
|
|
||||||
<a href="swift.org" class="text-sm">Swift</a>
|
|
||||||
"""
|
|
||||||
|
|
@ -4,5 +4,5 @@
|
||||||
/** You must specify `source(none)` after `tailwindcss` import and add `@source` for each source file / folder you want to include. */
|
/** You must specify `source(none)` after `tailwindcss` import and add `@source` for each source file / folder you want to include. */
|
||||||
@import "tailwindcss" source(none);
|
@import "tailwindcss" source(none);
|
||||||
@source "./Example.swift";
|
@source "./Example.swift";
|
||||||
@source "./Views";
|
@source "./Folder";
|
||||||
@source "./Views/**/*.swift";
|
@source "./Folder/**/*.swift";
|
||||||
|
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
import Foundation
|
|
||||||
|
|
||||||
extension ArtifactBundleBuilder {
|
|
||||||
func downloadFile(from urlString: String, to destination: String) throws {
|
|
||||||
guard let url = URL(string: urlString) else {
|
|
||||||
throw ArtifactBundleError.invalidURL(urlString)
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = try Data(contentsOf: url)
|
|
||||||
try data.write(to: URL(fileURLWithPath: destination))
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeExecutable(path: String) throws {
|
|
||||||
let attributes = [FileAttributeKey.posixPermissions: 0o755]
|
|
||||||
try fileManager.setAttributes(attributes, ofItemAtPath: path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func createZipFile(bundleDir: String, zipPath: String) throws {
|
|
||||||
// Remove existing ZIP file if it exists
|
|
||||||
if fileManager.fileExists(atPath: zipPath) {
|
|
||||||
try fileManager.removeItem(atPath: zipPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
let bundleDirURL = URL(fileURLWithPath: bundleDir)
|
|
||||||
let workDirURL = bundleDirURL.deletingLastPathComponent()
|
|
||||||
let bundleName = bundleDirURL.lastPathComponent
|
|
||||||
|
|
||||||
let process = Process()
|
|
||||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/zip")
|
|
||||||
process.arguments = ["-r", zipPath, bundleName]
|
|
||||||
process.currentDirectoryURL = workDirURL
|
|
||||||
|
|
||||||
try process.run()
|
|
||||||
process.waitUntilExit()
|
|
||||||
|
|
||||||
guard process.terminationStatus == 0 else {
|
|
||||||
throw ArtifactBundleError.zipCreationFailed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
import Crypto
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
extension ArtifactBundleBuilder {
|
|
||||||
/// Computes the SHA256 checksum of a file.
|
|
||||||
///
|
|
||||||
/// If `usingSHA256Directly` is true, it uses Swift Crypto's SHA256 implementation.
|
|
||||||
/// This is to workaround https://github.com/swiftlang/swift-package-manager/issues/9219.
|
|
||||||
func computeChecksum(
|
|
||||||
filePath: String,
|
|
||||||
usingSHA256Directly: Bool = false
|
|
||||||
) throws -> String {
|
|
||||||
if usingSHA256Directly {
|
|
||||||
// Use swift-crypto's SHA256 implementation
|
|
||||||
let fileURL = URL(fileURLWithPath: filePath)
|
|
||||||
let data = try Data(contentsOf: fileURL)
|
|
||||||
let hash = SHA256.hash(data: data)
|
|
||||||
return hash.compactMap { String(format: "%02x", $0) }.joined()
|
|
||||||
} else {
|
|
||||||
// Use swift package compute-checksum command
|
|
||||||
let process = Process()
|
|
||||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/swift")
|
|
||||||
process.arguments = ["package", "compute-checksum", filePath]
|
|
||||||
|
|
||||||
let pipe = Pipe()
|
|
||||||
process.standardOutput = pipe
|
|
||||||
|
|
||||||
try process.run()
|
|
||||||
process.waitUntilExit()
|
|
||||||
|
|
||||||
guard process.terminationStatus == 0 else {
|
|
||||||
throw ArtifactBundleError.checksumComputationFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
|
||||||
let output = String(data: data, encoding: .utf8)?.trimmingCharacters(
|
|
||||||
in: .whitespacesAndNewlines)
|
|
||||||
|
|
||||||
guard let checksum = output, !checksum.isEmpty else {
|
|
||||||
throw ArtifactBundleError.checksumComputationFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
return checksum
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
extension ArtifactBundleBuilder {
|
|
||||||
/// Expands a list of triples into a stricter list of triples.
|
|
||||||
///
|
|
||||||
/// To workaround https://github.com/swiftlang/swift-package-manager/issues/7362.
|
|
||||||
func expandingTriple(_ triple: String) -> [String] {
|
|
||||||
switch triple {
|
|
||||||
case "aarch64-apple-darwin":
|
|
||||||
[
|
|
||||||
"aarch64-apple-darwin",
|
|
||||||
"arm64-apple-macosx12.0",
|
|
||||||
"arm64-apple-macosx13.0",
|
|
||||||
"arm64-apple-macosx14.0",
|
|
||||||
"arm64-apple-macosx15.0",
|
|
||||||
"arm64-apple-macosx26.0",
|
|
||||||
]
|
|
||||||
case "x86_64-apple-darwin":
|
|
||||||
[
|
|
||||||
"x86_64-apple-darwin",
|
|
||||||
"x86_64-apple-macosx12.0",
|
|
||||||
"x86_64-apple-macosx13.0",
|
|
||||||
"x86_64-apple-macosx14.0",
|
|
||||||
"x86_64-apple-macosx15.0",
|
|
||||||
"x86_64-apple-macosx26.0",
|
|
||||||
]
|
|
||||||
// TODO: Does linux need more detailed triple variants?
|
|
||||||
default: [triple]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,158 +0,0 @@
|
||||||
import Foundation
|
|
||||||
|
|
||||||
class ArtifactBundleBuilder {
|
|
||||||
private let version: String
|
|
||||||
private let workDir: String
|
|
||||||
private let outputDir: String
|
|
||||||
let fileManager = FileManager.default
|
|
||||||
|
|
||||||
private let binaryConfigurations: [BinaryConfiguration] = [
|
|
||||||
BinaryConfiguration(binaryName: "tailwindcss-linux-x64", triple: "x86_64-unknown-linux-gnu"),
|
|
||||||
BinaryConfiguration(binaryName: "tailwindcss-macos-x64", triple: "x86_64-apple-darwin"),
|
|
||||||
BinaryConfiguration(binaryName: "tailwindcss-macos-arm64", triple: "aarch64-apple-darwin"),
|
|
||||||
]
|
|
||||||
|
|
||||||
init(version: String, workDir: String, outputDir: String) {
|
|
||||||
self.version = version
|
|
||||||
self.workDir = workDir
|
|
||||||
self.outputDir = outputDir
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildArtifactBundles() throws {
|
|
||||||
try setupWorkingDirectory()
|
|
||||||
|
|
||||||
var bundleInfos: [BundleInfo] = []
|
|
||||||
|
|
||||||
print("Creating individual bundles...")
|
|
||||||
for config in binaryConfigurations {
|
|
||||||
let bundleInfo = try createBundle(for: config)
|
|
||||||
bundleInfos.append(bundleInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
try generateArtifactBundleIndex(bundleInfos: bundleInfos)
|
|
||||||
|
|
||||||
print("=== BUILD COMPLETE ===")
|
|
||||||
print("All bundles created successfully:")
|
|
||||||
print("")
|
|
||||||
print("Generated artifact bundle index: \(outputDir)/tailwindcss.artifactbundleindex")
|
|
||||||
print("")
|
|
||||||
|
|
||||||
let indexChecksum = try computeChecksum(
|
|
||||||
filePath: "\(outputDir)/tailwindcss.artifactbundleindex", usingSHA256Directly: true)
|
|
||||||
print("Index checksum: \(indexChecksum)")
|
|
||||||
print("")
|
|
||||||
|
|
||||||
for bundleInfo in bundleInfos {
|
|
||||||
print("Bundle: \(bundleInfo.fileName)")
|
|
||||||
print(" Checksum: \(bundleInfo.checksum)")
|
|
||||||
print(" Triple: \(bundleInfo.triple)")
|
|
||||||
print("")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setupWorkingDirectory() throws {
|
|
||||||
print("Creating working directory: \(workDir)")
|
|
||||||
|
|
||||||
if fileManager.fileExists(atPath: workDir) {
|
|
||||||
try fileManager.removeItem(atPath: workDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
try fileManager.createDirectory(atPath: workDir, withIntermediateDirectories: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func createBundle(for config: BinaryConfiguration) throws -> BundleInfo {
|
|
||||||
let bundleDirName = "tailwindcss-\(version)-\(config.triple).artifactbundle"
|
|
||||||
let bundleDir = "\(workDir)/\(bundleDirName)"
|
|
||||||
let binaryPath = "bin/tailwindcss"
|
|
||||||
|
|
||||||
print("Creating bundle for \(config.triple)...")
|
|
||||||
|
|
||||||
// Create bundle directory structure
|
|
||||||
let binDir = "\(bundleDir)/bin"
|
|
||||||
try fileManager.createDirectory(atPath: binDir, withIntermediateDirectories: true)
|
|
||||||
|
|
||||||
// Download binary
|
|
||||||
print(" Downloading \(config.binaryName)...")
|
|
||||||
let binaryURL =
|
|
||||||
"https://github.com/tailwindlabs/tailwindcss/releases/download/\(version)/\(config.binaryName)"
|
|
||||||
let binaryDestination = "\(bundleDir)/\(binaryPath)"
|
|
||||||
|
|
||||||
try downloadFile(from: binaryURL, to: binaryDestination)
|
|
||||||
try makeExecutable(path: binaryDestination)
|
|
||||||
print(" ✓ Downloaded and made executable: \(binaryDestination)")
|
|
||||||
|
|
||||||
// Create info.json
|
|
||||||
print(" Creating info.json...")
|
|
||||||
try createInfoJSON(bundleDir: bundleDir, binaryPath: binaryPath, triple: config.triple)
|
|
||||||
print(
|
|
||||||
" ✓ Created info.json with version \(version), path \(binaryPath), triple \(config.triple)")
|
|
||||||
|
|
||||||
// Create ZIP file
|
|
||||||
let zipFileName = "\(bundleDirName).zip"
|
|
||||||
let zipPath = "\(outputDir)/\(zipFileName)"
|
|
||||||
print(" Creating ZIP file: \(zipPath)")
|
|
||||||
|
|
||||||
try createZipFile(bundleDir: bundleDir, zipPath: zipPath)
|
|
||||||
|
|
||||||
// Compute checksum
|
|
||||||
print(" Computing checksum...")
|
|
||||||
let checksum = try computeChecksum(filePath: zipPath)
|
|
||||||
|
|
||||||
print(" ✓ Bundle created: \(zipPath)")
|
|
||||||
print(" ✓ Checksum: \(checksum)")
|
|
||||||
print("")
|
|
||||||
|
|
||||||
return BundleInfo(fileName: zipFileName, checksum: checksum, triple: config.triple)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func createInfoJSON(bundleDir: String, binaryPath: String, triple: String) throws {
|
|
||||||
let artifact = Artifact(
|
|
||||||
version: version,
|
|
||||||
type: "executable",
|
|
||||||
variants: [
|
|
||||||
ArtifactVariant(path: binaryPath, supportedTriples: expandingTriple(triple))
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
let info = ArtifactBundleInfo(
|
|
||||||
schemaVersion: "1.0",
|
|
||||||
artifacts: ["tailwindcss": artifact]
|
|
||||||
)
|
|
||||||
|
|
||||||
let encoder = JSONEncoder()
|
|
||||||
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
|
||||||
|
|
||||||
let jsonData = try encoder.encode(info)
|
|
||||||
let infoPath = "\(bundleDir)/info.json"
|
|
||||||
try jsonData.write(to: URL(fileURLWithPath: infoPath))
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateArtifactBundleIndex(bundleInfos: [BundleInfo]) throws {
|
|
||||||
print("Generating tailwindcss.artifactbundleindex...")
|
|
||||||
|
|
||||||
// Create output directory if it doesn't exist
|
|
||||||
if !fileManager.fileExists(atPath: outputDir) {
|
|
||||||
try fileManager.createDirectory(atPath: outputDir, withIntermediateDirectories: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
let bundles = bundleInfos.map { bundleInfo in
|
|
||||||
Bundle(
|
|
||||||
fileName: bundleInfo.fileName,
|
|
||||||
checksum: bundleInfo.checksum,
|
|
||||||
supportedTriples: expandingTriple(bundleInfo.triple)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let index = ArtifactBundleIndex(
|
|
||||||
schemaVersion: "1.0",
|
|
||||||
archives: bundles
|
|
||||||
)
|
|
||||||
|
|
||||||
let encoder = JSONEncoder()
|
|
||||||
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
|
||||||
|
|
||||||
let jsonData = try encoder.encode(index)
|
|
||||||
let indexPath = "\(outputDir)/tailwindcss.artifactbundleindex"
|
|
||||||
try jsonData.write(to: URL(fileURLWithPath: indexPath))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import Foundation
|
|
||||||
|
|
||||||
enum ArtifactBundleError: Error, LocalizedError {
|
|
||||||
case invalidURL(String)
|
|
||||||
case zipCreationFailed
|
|
||||||
case checksumComputationFailed
|
|
||||||
|
|
||||||
var errorDescription: String? {
|
|
||||||
switch self {
|
|
||||||
case .invalidURL(let url):
|
|
||||||
return "Invalid URL: \(url)"
|
|
||||||
case .zipCreationFailed:
|
|
||||||
return "Failed to create ZIP file"
|
|
||||||
case .checksumComputationFailed:
|
|
||||||
return "Failed to compute checksum"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
import Foundation
|
|
||||||
|
|
||||||
// MARK: - Artifact Bundle Info Models
|
|
||||||
|
|
||||||
/// Represents the info.json file structure for an artifact bundle
|
|
||||||
struct ArtifactBundleInfo: Codable {
|
|
||||||
let schemaVersion: String
|
|
||||||
let artifacts: [String: Artifact]
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case schemaVersion, artifacts
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Artifact: Codable {
|
|
||||||
let version: String
|
|
||||||
let type: String
|
|
||||||
let variants: [ArtifactVariant]
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ArtifactVariant: Codable {
|
|
||||||
let path: String
|
|
||||||
let supportedTriples: [String]
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Artifact Bundle Index Models
|
|
||||||
|
|
||||||
/// Represents the .artifactbundleindex file structure
|
|
||||||
struct ArtifactBundleIndex: Codable {
|
|
||||||
let schemaVersion: String
|
|
||||||
let archives: [Bundle] // The proposal says it's "bundles" but the actual implementation uses "archives"
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Bundle: Codable {
|
|
||||||
let fileName: String
|
|
||||||
let checksum: String
|
|
||||||
let supportedTriples: [String]
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Internal Data Models
|
|
||||||
|
|
||||||
/// Configuration for a binary platform
|
|
||||||
struct BinaryConfiguration {
|
|
||||||
let binaryName: String
|
|
||||||
let triple: String
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Information about a created bundle
|
|
||||||
struct BundleInfo {
|
|
||||||
let fileName: String
|
|
||||||
let checksum: String
|
|
||||||
let triple: String
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import ArgumentParser
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
@main
|
|
||||||
struct TailwindCSSCLIArtifactBundler: ParsableCommand {
|
|
||||||
static let configuration = CommandConfiguration(
|
|
||||||
commandName: "TailwindCSSCLIArtifactBundler",
|
|
||||||
abstract: "Build TailwindCSS CLI artifact bundles for Swift Package Manager",
|
|
||||||
version: "1.0.0"
|
|
||||||
)
|
|
||||||
|
|
||||||
@Option(name: .shortAndLong, help: "TailwindCSS version to build (e.g., v4.1.14)")
|
|
||||||
var version: String
|
|
||||||
|
|
||||||
@Option(name: .shortAndLong, help: "Working directory for temporary files")
|
|
||||||
var workDir: String = "/tmp/tailwindcss-bundles"
|
|
||||||
|
|
||||||
@Option(name: .shortAndLong, help: "Output directory for the artifact bundle index")
|
|
||||||
var outputDir: String = "."
|
|
||||||
|
|
||||||
func run() throws {
|
|
||||||
print("Building artifact bundles for TailwindCSS version: \(version)")
|
|
||||||
|
|
||||||
let bundler = ArtifactBundleBuilder(
|
|
||||||
version: version,
|
|
||||||
workDir: workDir,
|
|
||||||
outputDir: outputDir
|
|
||||||
)
|
|
||||||
|
|
||||||
try bundler.buildArtifactBundles()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -21,10 +21,6 @@ struct SwiftTailwindExampleTests {
|
||||||
generatedCSS?.contains("bg-red-100") == true,
|
generatedCSS?.contains("bg-red-100") == true,
|
||||||
"Arbitary value class used in Folder/Template.swift is generated."
|
"Arbitary value class used in Folder/Template.swift is generated."
|
||||||
)
|
)
|
||||||
#expect(
|
|
||||||
generatedCSS?.contains("text-sm") == false,
|
|
||||||
"Class used in other non-included Swift code is not generated."
|
|
||||||
)
|
|
||||||
#expect(
|
#expect(
|
||||||
generatedCSS?.contains("bg-blue-500") == false,
|
generatedCSS?.contains("bg-blue-500") == false,
|
||||||
"Class not used is not generated."
|
"Class not used is not generated."
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue