From cfb68f5237c89dd2e047e47d7c385cbe6abf9557 Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Mon, 5 Sep 2022 15:56:05 +0200 Subject: [PATCH] Minify JS and CSS files --- .../Extensions/URL+Extensions.swift | 12 ++- WebsiteGenerator/Files/Configuration.swift | 2 + WebsiteGenerator/Files/FileSystem.swift | 101 ++++++++++++++++-- WebsiteGenerator/main.swift | 4 +- 4 files changed, 107 insertions(+), 12 deletions(-) diff --git a/WebsiteGenerator/Extensions/URL+Extensions.swift b/WebsiteGenerator/Extensions/URL+Extensions.swift index f88a55f..9281cb5 100644 --- a/WebsiteGenerator/Extensions/URL+Extensions.swift +++ b/WebsiteGenerator/Extensions/URL+Extensions.swift @@ -14,7 +14,17 @@ extension URL { } var isDirectory: Bool { - (try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true + do { + let resources = try resourceValues(forKeys: [.isDirectoryKey]) + guard let isDirectory = resources.isDirectory else { + print("No isDirectory info for \(path)") + return false + } + return isDirectory + } catch { + print("Failed to get directory information from \(path): \(error)") + return false + } } var exists: Bool { diff --git a/WebsiteGenerator/Files/Configuration.swift b/WebsiteGenerator/Files/Configuration.swift index 554b786..d83cec2 100644 --- a/WebsiteGenerator/Files/Configuration.swift +++ b/WebsiteGenerator/Files/Configuration.swift @@ -3,4 +3,6 @@ import Foundation struct Configuration { let pageImageWidth: Int + + let minifyCSSandJS: Bool } diff --git a/WebsiteGenerator/Files/FileSystem.swift b/WebsiteGenerator/Files/FileSystem.swift index da38c6c..839c702 100644 --- a/WebsiteGenerator/Files/FileSystem.swift +++ b/WebsiteGenerator/Files/FileSystem.swift @@ -7,6 +7,8 @@ typealias SourceTextFile = (content: String, didChange: Bool) final class FileSystem { + private static let tempFileName = "temp.bin" + private static let hashesFileName = "hashes.json" private let input: URL @@ -19,6 +21,10 @@ final class FileSystem { input.appendingPathComponent(FileSystem.hashesFileName) } + private var tempFile: URL { + input.appendingPathComponent(FileSystem.tempFileName) + } + /** The hashes of all accessed files from the previous run @@ -357,7 +363,21 @@ final class FileSystem { Add a file as required, so that it will be copied to the output directory. */ func require(file: String) { - requiredFiles.insert(file) + let url = input.appendingPathComponent(file) + guard url.exists, url.isDirectory else { + requiredFiles.insert(file) + return + } + do { + try FileManager.default + .contentsOfDirectory(atPath: url.path) + .forEach { + // Recurse into subfolders + require(file: file + "/" + $0) + } + } catch { + log.add(error: "Failed to read folder \(file): \(error)", source: source) + } } /** @@ -391,17 +411,11 @@ final class FileSystem { } continue } - let data: Data - do { - data = try Data(contentsOf: sourceUrl) - } catch { - log.add(error: "Failed to read data at \(sourceUrl.path)", source: source, error: error) - continue - } - if writeIfChanged(data, to: destinationUrl) { + if copyFileIfChanged(from: sourceUrl, to: destinationUrl) { copiedFiles.insert(file) } } + try? tempFile.delete() for (file, source) in expectedFiles { guard !isExternal(file: file) else { continue @@ -422,6 +436,52 @@ final class FileSystem { } } + private func copyFileIfChanged(from sourceUrl: URL, to destinationUrl: URL) -> Bool { + guard configuration.minifyCSSandJS else { + return copyBinaryFileIfChanged(from: sourceUrl, to: destinationUrl) + } + switch sourceUrl.pathExtension.lowercased() { + case "js": + return minifyJS(at: sourceUrl, andWriteTo: destinationUrl) + case "css": + return minifyCSS(at: sourceUrl, andWriteTo: destinationUrl) + default: + return copyBinaryFileIfChanged(from: sourceUrl, to: destinationUrl) + } + } + + private func copyBinaryFileIfChanged(from sourceUrl: URL, to destinationUrl: URL) -> Bool { + do { + let data = try Data(contentsOf: sourceUrl) + return writeIfChanged(data, to: destinationUrl) + } catch { + log.add(error: "Failed to read data at \(sourceUrl.path)", source: source, error: error) + return false + } + } + + private func minifyJS(at sourceUrl: URL, andWriteTo destinationUrl: URL) -> Bool { + let command = "uglifyjs \(sourceUrl.path) > \(tempFile.path)" + do { + _ = try safeShell(command) + return copyBinaryFileIfChanged(from: tempFile, to: destinationUrl) + } catch { + log.add(error: "Failed to minify \(sourceUrl.path): \(error)", source: source) + return false + } + } + + private func minifyCSS(at sourceUrl: URL, andWriteTo destinationUrl: URL) -> Bool { + let command = "cleancss \(sourceUrl.path) -o \(tempFile.path)" + do { + _ = try safeShell(command) + return copyBinaryFileIfChanged(from: tempFile, to: destinationUrl) + } catch { + log.add(error: "Failed to minify \(sourceUrl.path): \(error)", source: source) + return false + } + } + private func cleanRelativeURL(_ raw: String) -> String { let raw = raw.dropAfterLast("#") // Clean links to page content guard raw.contains("..") else { @@ -468,7 +528,7 @@ final class FileSystem { guard !externalFiles.isEmpty else { return } - print("\(externalFiles.count) external files needed:") + print("\(externalFiles.count) external resources needed:") for file in externalFiles.sorted() { print(" " + file) } @@ -553,6 +613,27 @@ final class FileSystem { let data = string.data(using: .utf8)! return writeIfChanged(data, to: url) } + + // MARK: Running other tasks + + @discardableResult + func safeShell(_ command: String) throws -> String { + let task = Process() + let pipe = Pipe() + + task.standardOutput = pipe + task.standardError = pipe + task.arguments = ["-cl", command] + task.executableURL = URL(fileURLWithPath: "/bin/zsh") + task.standardInput = nil + + try task.run() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let output = String(data: data, encoding: .utf8)! + + return output + } } private extension Digest { diff --git a/WebsiteGenerator/main.swift b/WebsiteGenerator/main.swift index f67d54f..913fc7d 100644 --- a/WebsiteGenerator/main.swift +++ b/WebsiteGenerator/main.swift @@ -3,7 +3,9 @@ import Foundation private let contentDirectory = URL(fileURLWithPath: "/Users/ch/Projects/MakerSpace") private let outputDirectory = URL(fileURLWithPath: "/Users/ch/Projects/MakerSpace/Site") -let configuration = Configuration(pageImageWidth: 748) +let configuration = Configuration( + pageImageWidth: 748, + minifyCSSandJS: true) let log = ValidationLog() let files = FileSystem(in: contentDirectory, to: outputDirectory)