diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index d57fb340..c19b1c00 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -5,35 +5,23 @@ env: IMAGE_ID: $IMAGE_ID steps: - - label: ':react: Build React App' - command: make build REFRESH_L10N=1 - plugins: &plugins - - $CI_TOOLKIT_PLUGIN - - $NVM_PLUGIN - - - label: ':eslint: Lint React App' - command: make lint-js - plugins: *plugins - - label: ':javascript: Test JavaScript' command: make test-js - plugins: *plugins + plugins: &plugins + - $CI_TOOLKIT_PLUGIN + - $NVM_PLUGIN - - label: ':android: Publish Android Library' - command: | - make build REFRESH_L10N=1 - echo "--- :android: Publishing Android Library" - ./android/gradlew -p ./android :gutenberg:prepareToPublishToS3 $(prepare_to_publish_to_s3_params) :gutenberg:publish - agents: - queue: android + - label: ':swift: Test Swift Package' + # With the HTML assets in the GutenbergKitResources package and ignored by Git, we need to generated on demand for the moment + command: make build && swift test plugins: *plugins - - label: ':android: Test Android Library' - command: make test-android - agents: - queue: android + - label: ':swift: Test Swift Package with XCFramework (hardcoded)' + command: swift test plugins: *plugins - - label: ':swift: Test Swift Package' - command: swift test + - label: ':xcode: Build XCFramework' + command: make build-resources-xcframework plugins: *plugins + artifact_paths: + - 'build/*.xcframework.zip' diff --git a/.gitignore b/.gitignore index 152f1095..cca1d785 100644 --- a/.gitignore +++ b/.gitignore @@ -188,6 +188,8 @@ local.properties ## Production Build Products /android/Gutenberg/src/main/assets/assets /android/Gutenberg/src/main/assets/index.html +/ios/Sources/GutenbergKitResources/Resources/assets +/ios/Sources/GutenbergKitResources/Resources/index.html # Disabled removing these files until this is published like Android in CI. # /ios/Sources/GutenbergKit/Gutenberg/assets diff --git a/Makefile b/Makefile index 7882bbef..6af853f1 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,11 @@ .DEFAULT_GOAL := help SIMULATOR_DESTINATION := OS=26.0,name=iPhone 17 +GUTENBERG_RESOURCES_XCFRAMEWORK_NAME := GutenbergKitResources + +# Use local resources instead of pre-built XCFramework for Swift package. +# After all, this is the automation that builds the XCFramework, among others. +export GUTENBERGKIT_SWIFT_USE_LOCAL_RESOURCES := 1 .PHONY: help help: ## Display this help menu @@ -76,12 +81,18 @@ build: npm-dependencies prep-translations ## Build the project for all platforms @echo "--- :open_file_folder: Copying Build Products into place" rm -rf ./ios/Sources/GutenbergKit/Gutenberg/ ./android/Gutenberg/src/main/assets/ cp -r ./dist/. ./ios/Sources/GutenbergKit/Gutenberg/ + cp -r ./dist/. "./ios/Sources/${GUTENBERG_RESOURCES_XCFRAMEWORK_NAME}/Resources/" cp -r ./dist/. ./android/Gutenberg/src/main/assets .PHONY: build-swift-package -build-swift-package: build ## Build the Swift package for iOS +build-swift-package: build-resources-xcframework ## Build the Swift package for iOS $(call XCODEBUILD_CMD, build) +.PHONY: build-resources-xcframework +build-resources-xcframework: build # Build the resources XCFramework + @echo "--- :package: Building Gutenberg resources XCFramework" + @SWIFT_OPTIMIZATION_LEVEL="${SWIFT_OPTIMIZATION_LEVEL:--O}" ./build_xcframework.sh ${GUTENBERG_RESOURCES_XCFRAMEWORK_NAME} + .PHONY: local-android-library local-android-library: build ## Build the Android library to local Maven @echo "--- :android: Building Library" diff --git a/Package.swift b/Package.swift index 9bb02fcb..df624f56 100644 --- a/Package.swift +++ b/Package.swift @@ -2,13 +2,51 @@ // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription +import Foundation + +// Set GUTENBERGKIT_SWIFT_USE_LOCAL_RESOURCES=1 to build resources from source instead of using the pre-built XCFramework +let useLocalResources = ProcessInfo.processInfo.environment["GUTENBERGKIT_SWIFT_USE_LOCAL_RESOURCES"] != nil + +// TODO: This has been manually uploaded, we'll need automation to both upload and update the URL and checksum +let revision = "e0c9c4d8df3d6fc607e5011f1fbdf1791159c5b3" +let xcframeworkURL = "https://cdn.a8c-ci.services/gutenbergkit/GutenbergKitResources-\(revision).xcframework.zip" + +let xcframeworkChecksum = "270fb3f8a4b1db7be8a29f5c7a28dd5ce2127a2072c0e1bf95b7ddae7e8d7f9c" + +// Only expose GutenbergKitResources as a product when building from source (needed for XCFramework generation) +let resourcesProducts: [Product] = useLocalResources + ? [ + .library( + name: "GutenbergKitResources", + // Required for XCFramework generation + type: .dynamic, + targets: ["GutenbergKitResources"] + ) + ] + : [] + +let resourcesTargets: [Target] = useLocalResources + ? [ + .target( + name: "GutenbergKitResources", + path: "ios/Sources/GutenbergKitResources", + resources: [.copy("Resources")] + ) + ] + : [ + .binaryTarget( + name: "GutenbergKitResources", + url: xcframeworkURL, + checksum: xcframeworkChecksum + ) + ] let package = Package( name: "GutenbergKit", platforms: [.iOS(.v17), .macOS(.v14)], products: [ - .library(name: "GutenbergKit", targets: ["GutenbergKit"]) - ], + .library(name: "GutenbergKit", targets: ["GutenbergKit"]), + ] + resourcesProducts, dependencies: [ .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.7.5"), .package(url: "https://github.com/exyte/SVGView.git", from: "1.0.6"), @@ -16,10 +54,20 @@ let package = Package( targets: [ .target( name: "GutenbergKit", - dependencies: ["SwiftSoup", "SVGView"], + dependencies: [ + "SwiftSoup", + "SVGView", + "GutenbergKitResources" + ], path: "ios/Sources/GutenbergKit", exclude: [], - resources: [.copy("Gutenberg")] + resources: [.copy("Gutenberg")], + // Required to allow importing GutenbergKitResources when it's a binary target (XCFramework). + // Without this, Swift fails with "module was built from a non-package interface" because + // it treats both targets as same-package but the XCFramework was built for distribution. + // Note: This means GutenbergKit source cannot use the `package` access modifier. + // See: https://developer.apple.com/documentation/packagedescription/target/packageaccess + packageAccess: false ), .testTarget( name: "GutenbergKitTests", @@ -29,6 +77,6 @@ let package = Package( resources: [ .process("Resources") ] - ) - ] + ), + ] + resourcesTargets ) diff --git a/build_xcframework.sh b/build_xcframework.sh new file mode 100755 index 00000000..c9049d6b --- /dev/null +++ b/build_xcframework.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Colors for output +GREEN='\033[0;32m' +NC='\033[0m' # No Color + +# Originally sourced from: +# https://github.com/OpenSwiftUIProject/ProtobufKit/blob/937eae5426277bec040c7f99bc8e1498c30ed467/Scripts/build_xcframework.sh +# +# Found it via: +# https://forums.swift.org/t/how-on-earth-can-i-create-a-framework-from-a-swift-package/76797/6 +# +# Related: +# https://forums.swift.org/t/how-to-build-swift-package-as-xcframework/41414/57 + +# Script modified from https://docs.emergetools.com/docs/analyzing-a-spm-framework-ios + +PACKAGE_NAME=${1-} +if [ -z "$PACKAGE_NAME" ]; then + echo "No package name provided. Using the first scheme found in the Package.swift." + PACKAGE_NAME=$(xcodebuild -list | awk 'schemes && NF>0 { print $1; exit } /Schemes:$/ { schemes = 1 }') + echo "Using: $PACKAGE_NAME" +fi + +# Swift optimization level: -Onone (no optimization), -O (optimize for speed), -Osize (optimize for size) +# Default to -O for release builds, can be overridden with SWIFT_OPTIMIZATION_LEVEL environment variable +SWIFT_OPTIMIZATION_LEVEL="${SWIFT_OPTIMIZATION_LEVEL:--O}" +echo "Swift optimization level: $SWIFT_OPTIMIZATION_LEVEL" + +# FIXME: Original script was in subfolder, this is in repo root for the time being. +# +# SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd -P)" +# PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +PROJECT_ROOT=$(pwd) + +PROJECT_BUILD_DIR="${PROJECT_BUILD_DIR:-"${PROJECT_ROOT}/build"}" +XCODEBUILD_BUILD_DIR="$PROJECT_BUILD_DIR/xcodebuild" +XCODEBUILD_DERIVED_DATA_PATH="$XCODEBUILD_BUILD_DIR/DerivedData" + +echo "PROJECT_BUILD_DIR is $PROJECT_BUILD_DIR" + +build_framework() { + local sdk="$1" + local destination="$2" + local scheme="$3" + + echo "--- Build framework for $scheme $sdk $destination" + + local XCODEBUILD_ARCHIVE_PATH="./build/$scheme-$sdk.xcarchive" + + rm -rf "$XCODEBUILD_ARCHIVE_PATH" + + # TODO: Consider using this env var to switch between static (default) + # and dynamic (required for XCFramework) + # + # See: + # https://github.com/OpenSwiftUIProject/ProtobufKit/blob/937eae5426277bec040c7f99bc8e1498c30ed467/Package.swift#L30 + # LIBRARY_TYPE=dynamic xcodebuild archive \ + xcodebuild archive \ + -scheme "$scheme" \ + -archivePath "$XCODEBUILD_ARCHIVE_PATH" \ + -derivedDataPath "$XCODEBUILD_DERIVED_DATA_PATH" \ + -sdk "$sdk" \ + -destination "$destination" \ + BUILD_LIBRARY_FOR_DISTRIBUTION=YES \ + INSTALL_PATH='Library/Frameworks' \ + SWIFT_OPTIMIZATION_LEVEL="$SWIFT_OPTIMIZATION_LEVEL" \ + OTHER_SWIFT_FLAGS=-no-verify-emitted-module-interface \ + CODE_SIGN_IDENTITY="-" \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=NO \ + | xcbeautify + + if [ "$sdk" = "macosx" ]; then + FRAMEWORK_MODULES_PATH="$XCODEBUILD_ARCHIVE_PATH/Products/Library/Frameworks/$scheme.framework/Versions/Current/Modules" + mkdir -p "$FRAMEWORK_MODULES_PATH" + cp -r \ + "$XCODEBUILD_DERIVED_DATA_PATH/Build/Intermediates.noindex/ArchiveIntermediates/$scheme/BuildProductsPath/Release/$scheme.swiftmodule" \ + "$FRAMEWORK_MODULES_PATH/$scheme.swiftmodule" + rm -rf "$XCODEBUILD_ARCHIVE_PATH/Products/Library/Frameworks/$scheme.framework/Modules" + ln -s Versions/Current/Modules "$XCODEBUILD_ARCHIVE_PATH/Products/Library/Frameworks/$scheme.framework/Modules" + else + FRAMEWORK_MODULES_PATH="$XCODEBUILD_ARCHIVE_PATH/Products/Library/Frameworks/$scheme.framework/Modules" + mkdir -p "$FRAMEWORK_MODULES_PATH" + cp -r \ + "$XCODEBUILD_DERIVED_DATA_PATH/Build/Intermediates.noindex/ArchiveIntermediates/$scheme/BuildProductsPath/Release-$sdk/$scheme.swiftmodule" \ + "$FRAMEWORK_MODULES_PATH/$scheme.swiftmodule" + fi + + # Delete private and package swiftinterface + rm -f "$FRAMEWORK_MODULES_PATH/$scheme.swiftmodule/*.package.swiftinterface" + rm -f "$FRAMEWORK_MODULES_PATH/$scheme.swiftmodule/*.private.swiftinterface" +} + +copy_resource_bundles() { + local sdk="$1" + local scheme="$2" + + echo "--- Copy resource bundles for $scheme $sdk" + + local XCODEBUILD_ARCHIVE_PATH="./build/$scheme-$sdk.xcarchive" + local FRAMEWORK_PATH="$XCODEBUILD_ARCHIVE_PATH/Products/Library/Frameworks/$scheme.framework" + + # Find all resource bundles in DerivedData + local BUNDLE_PATH="$XCODEBUILD_DERIVED_DATA_PATH/Build/Intermediates.noindex/ArchiveIntermediates/$scheme/IntermediateBuildFilesPath/UninstalledProducts/$sdk" + + # Copy all .bundle files found + if [ -d "$BUNDLE_PATH" ]; then + find "$BUNDLE_PATH" -name "*.bundle" -maxdepth 1 -type d -print0 | while IFS= read -r -d '' bundle; do + bundle_name=$(basename "$bundle") + echo "Copying resource bundle: $bundle_name to $FRAMEWORK_PATH" + # Remove symlink if it exists and copy the actual bundle + rm -rf "${FRAMEWORK_PATH:?}/$bundle_name" + cp -R "$bundle" "$FRAMEWORK_PATH/" + done + else + echo "Warning: Bundle path not found: $BUNDLE_PATH" + fi +} + +build_framework "iphonesimulator" "generic/platform=iOS Simulator" "$PACKAGE_NAME" +copy_resource_bundles "iphonesimulator" "$PACKAGE_NAME" + +build_framework "iphoneos" "generic/platform=iOS" "$PACKAGE_NAME" +copy_resource_bundles "iphoneos" "$PACKAGE_NAME" + +# No macOS support because of UIKit in the dependencies +# +# build_framework "macosx" "generic/platform=macOS" "$PACKAGE_NAME" +# copy_resource_bundles "macosx" "$PACKAGE_NAME" + +echo "Builds completed successfully." + +pushd "$PROJECT_BUILD_DIR" > /dev/null + +rm -rf "$PACKAGE_NAME.xcframework" +xcodebuild -create-xcframework \ + -framework "$PACKAGE_NAME-iphonesimulator.xcarchive/Products/Library/Frameworks/$PACKAGE_NAME.framework" \ + -framework "$PACKAGE_NAME-iphoneos.xcarchive/Products/Library/Frameworks/$PACKAGE_NAME.framework" \ + -output "$PACKAGE_NAME.xcframework" + +cp -r "$PACKAGE_NAME-iphonesimulator.xcarchive/dSYMs" "$PACKAGE_NAME.xcframework/ios-arm64_x86_64-simulator" +cp -r "$PACKAGE_NAME-iphoneos.xcarchive/dSYMs" "$PACKAGE_NAME.xcframework/ios-arm64" + +GIT_SHA=$(git rev-parse HEAD) +ZIP_NAME="$PACKAGE_NAME-$GIT_SHA.xcframework.zip" +zip -r "$ZIP_NAME" "$PACKAGE_NAME.xcframework" > /dev/null + +CHECKSUM=$(swift package compute-checksum "$ZIP_NAME") + +echo -e "${GREEN}XCFramework generated at $(pwd)/$PACKAGE_NAME.xcframework${NC}" +echo -e "${GREEN}Zip archive: $(pwd)/$ZIP_NAME${NC}" + +echo "+++ :swift: XCFramework checksum" +echo "$CHECKSUM" + +popd > /dev/null diff --git a/ios/Sources/GutenbergKit/Sources/Model/EditorCachePolicy.swift b/ios/Sources/GutenbergKit/Sources/Model/EditorCachePolicy.swift index 3dc21c3f..7cd79301 100644 --- a/ios/Sources/GutenbergKit/Sources/Model/EditorCachePolicy.swift +++ b/ios/Sources/GutenbergKit/Sources/Model/EditorCachePolicy.swift @@ -94,7 +94,7 @@ public enum EditorCachePolicy: Sendable { /// (i.e., the cached response hasn't expired yet). /// - ``always``: Always returns `true` - cached responses are always used. /// - package func allowsResponseWith(date: Date, currentDate: Date = .now) -> Bool { + internal func allowsResponseWith(date: Date, currentDate: Date = .now) -> Bool { switch self { case .ignore: false case .maxAge(let interval): date.addingTimeInterval(interval) > currentDate diff --git a/ios/Sources/GutenbergKit/Sources/Model/EditorConfiguration.swift b/ios/Sources/GutenbergKit/Sources/Model/EditorConfiguration.swift index e1afcad8..0b176e9b 100644 --- a/ios/Sources/GutenbergKit/Sources/Model/EditorConfiguration.swift +++ b/ios/Sources/GutenbergKit/Sources/Model/EditorConfiguration.swift @@ -47,7 +47,7 @@ public struct EditorConfiguration: Sendable, Hashable, Equatable { /// Don't make HTTP requests public let isOfflineModeEnabled: Bool /// A site ID derived from the URL that can be used in file system paths - package let siteId: String + internal let siteId: String /// Deliberately non-public – consumers should use `EditorConfigurationBuilder` to construct a configuration init( @@ -125,11 +125,11 @@ public struct EditorConfiguration: Sendable, Hashable, Equatable { ) } - package var escapedTitle: String { + internal var escapedTitle: String { title.addingPercentEncoding(withAllowedCharacters: .alphanumerics)! } - package var escapedContent: String { + internal var escapedContent: String { content.addingPercentEncoding(withAllowedCharacters: .alphanumerics)! } } diff --git a/ios/Sources/GutenbergKit/Sources/Model/EditorProgress.swift b/ios/Sources/GutenbergKit/Sources/Model/EditorProgress.swift index d2a8a727..96205495 100644 --- a/ios/Sources/GutenbergKit/Sources/Model/EditorProgress.swift +++ b/ios/Sources/GutenbergKit/Sources/Model/EditorProgress.swift @@ -25,7 +25,7 @@ public struct EditorProgress: Codable, Sendable, Equatable { /// - Parameters: /// - completed: The number of completed items. /// - total: The total number of items. - package init(completed: Int, total: Int) { + internal init(completed: Int, total: Int) { self.completed = completed self.total = total } diff --git a/ios/Sources/GutenbergKit/Sources/RESTAPIRepository.swift b/ios/Sources/GutenbergKit/Sources/RESTAPIRepository.swift index a65df4d1..6a2d7b85 100644 --- a/ios/Sources/GutenbergKit/Sources/RESTAPIRepository.swift +++ b/ios/Sources/GutenbergKit/Sources/RESTAPIRepository.swift @@ -8,7 +8,7 @@ import Foundation /// and returned on subsequent requests to improve loading performance. public struct RESTAPIRepository: Sendable { - package let httpClient: EditorHTTPClientProtocol + internal let httpClient: EditorHTTPClientProtocol private let configuration: EditorConfiguration private let cache: EditorURLCache @@ -89,11 +89,11 @@ public struct RESTAPIRepository: Sendable { // MARK: GET Post Type @discardableResult - package func fetchPostType(for type: String) async throws -> EditorURLResponse { + internal func fetchPostType(for type: String) async throws -> EditorURLResponse { try await self.perform(method: .GET, url: self.buildPostTypeUrl(type: type)) } - package func readPostType(for type: String) throws -> EditorURLResponse? { + internal func readPostType(for type: String) throws -> EditorURLResponse? { try self.cache.response(for: buildPostTypeUrl(type: type), httpMethod: .GET) } @@ -107,31 +107,31 @@ public struct RESTAPIRepository: Sendable { // MARK: GET Active Theme @discardableResult - package func fetchActiveTheme() async throws -> EditorURLResponse { + internal func fetchActiveTheme() async throws -> EditorURLResponse { try await self.perform(method: .GET, url: self.activeThemeUrl) } - package func readActiveTheme() throws -> EditorURLResponse? { + internal func readActiveTheme() throws -> EditorURLResponse? { try self.cache.response(for: self.activeThemeUrl, httpMethod: .GET) } // MARK: OPTIONS Settings @discardableResult - package func fetchSettingsOptions() async throws -> EditorURLResponse { + internal func fetchSettingsOptions() async throws -> EditorURLResponse { try await self.perform(method: .OPTIONS, url: self.siteSettingsUrl) } - package func readSettingsOptions() throws -> EditorURLResponse? { + internal func readSettingsOptions() throws -> EditorURLResponse? { try self.cache.response(for: self.siteSettingsUrl, httpMethod: .OPTIONS) } // MARK: Post Types @discardableResult - package func fetchPostTypes() async throws -> EditorURLResponse { + internal func fetchPostTypes() async throws -> EditorURLResponse { try await self.perform(method: .GET, url: self.postTypesUrl) } - package func readPostTypes() throws -> EditorURLResponse? { + internal func readPostTypes() throws -> EditorURLResponse? { try self.cache.response(for: self.postTypesUrl, httpMethod: .GET) } diff --git a/ios/Sources/GutenbergKit/Sources/Stores/EditorAssetLibrary.swift b/ios/Sources/GutenbergKit/Sources/Stores/EditorAssetLibrary.swift index 5c598c8e..fd2cc8c5 100644 --- a/ios/Sources/GutenbergKit/Sources/Stores/EditorAssetLibrary.swift +++ b/ios/Sources/GutenbergKit/Sources/Stores/EditorAssetLibrary.swift @@ -36,7 +36,7 @@ public actor EditorAssetLibrary { /// /// Applications should periodically check for a new editor manifest. This can be very expensive, so this method defaults to returning an existing one on-disk. /// - package func fetchManifest() async throws -> LocalEditorAssetManifest { + internal func fetchManifest() async throws -> LocalEditorAssetManifest { guard configuration.shouldUsePlugins else { return .empty } let data = try await httpClient.perform( URLRequest(method: .GET, url: self.editorAssetsUrl(for: self.configuration)) @@ -83,13 +83,13 @@ public actor EditorAssetLibrary { /// Checks whether a bundle with the given manifest checksum exists on disk. /// - package func hasBundle(forManifestChecksum checksum: String) -> Bool { + internal func hasBundle(forManifestChecksum checksum: String) -> Bool { FileManager.default.directoryExists(at: self.bundleRoot(for: checksum)) } /// Retrieves an existing bundle from disk if one exists for the given manifest checksum. /// - package func existingBundle(forManifestChecksum checksum: String) throws -> EditorAssetBundle? { + internal func existingBundle(forManifestChecksum checksum: String) throws -> EditorAssetBundle? { guard self.hasBundle(forManifestChecksum: checksum) else { return nil } @@ -103,7 +103,7 @@ public actor EditorAssetLibrary { /// /// Assets are downloaded concurrently and stored in a temporary directory. Once all downloads /// complete successfully, the bundle is atomically moved to its final location. - package func buildBundle( + internal func buildBundle( for manifest: LocalEditorAssetManifest, progress: EditorProgressCallback? = nil ) async throws -> EditorAssetBundle { diff --git a/ios/Sources/GutenbergKit/Sources/Stores/EditorURLCache.swift b/ios/Sources/GutenbergKit/Sources/Stores/EditorURLCache.swift index 2136d636..c598055b 100644 --- a/ios/Sources/GutenbergKit/Sources/Stores/EditorURLCache.swift +++ b/ios/Sources/GutenbergKit/Sources/Stores/EditorURLCache.swift @@ -39,7 +39,7 @@ public struct EditorURLCache: Sendable { try self.store(response, for: url, httpMethod: httpMethod, currentDate: .now) } - package func store( + internal func store( _ response: EditorURLResponse, for url: URL, httpMethod: EditorHttpMethod, @@ -82,7 +82,7 @@ public struct EditorURLCache: Sendable { try self.store(fileAt: path, headers: headers, for: url, httpMethod: httpMethod, currentDate: .now) } - package func store( + internal func store( fileAt path: URL, headers: EditorHTTPHeaders, for url: URL, @@ -119,7 +119,7 @@ public struct EditorURLCache: Sendable { try self.response(for: url, httpMethod: httpMethod, currentDate: .now) != nil } - package func hasData(for url: URL, httpMethod: EditorHttpMethod, currentDate: Date) throws -> Bool { + internal func hasData(for url: URL, httpMethod: EditorHttpMethod, currentDate: Date) throws -> Bool { try self.response(for: url, httpMethod: httpMethod, currentDate: currentDate) != nil } @@ -134,7 +134,7 @@ public struct EditorURLCache: Sendable { try self.response(for: url, httpMethod: httpMethod, currentDate: .now) } - package func response( + internal func response( for url: URL, httpMethod: EditorHttpMethod, currentDate: Date diff --git a/ios/Sources/GutenbergKit/Sources/Views/HTMLPreview/HTMLPreviewManager.swift b/ios/Sources/GutenbergKit/Sources/Views/HTMLPreview/HTMLPreviewManager.swift index 96aa40bc..8851f375 100644 --- a/ios/Sources/GutenbergKit/Sources/Views/HTMLPreview/HTMLPreviewManager.swift +++ b/ios/Sources/GutenbergKit/Sources/Views/HTMLPreview/HTMLPreviewManager.swift @@ -5,6 +5,7 @@ import CryptoKit import ImageIO import UniformTypeIdentifiers import SwiftUI +import GutenbergKitResources /// Renders HTML content to images using a pool of WKWebView instances. /// @@ -52,7 +53,7 @@ public final class HTMLPreviewManager: ObservableObject { // MARK: - Initialization public init(themeStyles: String? = nil) { - let gutenbergCSS = Self.loadGutenbergCSS() ?? "" + let gutenbergCSS = GutenbergKitResources.loadGutenbergCSS() ?? "" assert(!gutenbergCSS.isEmpty, "Failed to load Gutenberg CSS from bundle. Previews will not render correctly.") self.editorStyles = gutenbergCSS @@ -67,23 +68,6 @@ public final class HTMLPreviewManager: ObservableObject { self.urlCache = HTMLPreviewManager.makeCache() } - /// Loads the Gutenberg CSS from the bundled assets - private static func loadGutenbergCSS() -> String? { - guard let assetsURL = Bundle.module.url(forResource: "Gutenberg", withExtension: nil) else { - assertionFailure("Gutenberg resource not found in bundle") - return nil - } - - let assetsDirectory = assetsURL.appendingPathComponent("assets") - guard let files = try? FileManager.default.contentsOfDirectory(at: assetsDirectory, includingPropertiesForKeys: nil), - let cssURL = files.first(where: { $0.lastPathComponent.hasPrefix("index-") && $0.lastPathComponent.hasSuffix(".css") }), - let css = try? String(contentsOf: cssURL, encoding: .utf8) else { - assertionFailure("Failed to load Gutenberg CSS from bundle") - return nil - } - return css - } - // MARK: - Public API /// Renders HTML content to an image diff --git a/ios/Sources/GutenbergKitResources/Resources/.gitkeep b/ios/Sources/GutenbergKitResources/Resources/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/ios/Sources/GutenbergKitResources/Sources/GutenbergKitResources.swift b/ios/Sources/GutenbergKitResources/Sources/GutenbergKitResources.swift new file mode 100644 index 00000000..75e52450 --- /dev/null +++ b/ios/Sources/GutenbergKitResources/Sources/GutenbergKitResources.swift @@ -0,0 +1,21 @@ +import Foundation + +public struct GutenbergKitResources { + + /// Loads the Gutenberg CSS from the bundled assets. + public static func loadGutenbergCSS() -> String? { + guard let assetsURL = Bundle.module.url(forResource: "Gutenberg", withExtension: nil) else { + assertionFailure("Gutenberg resource not found in bundle") + return nil + } + + let assetsDirectory = assetsURL.appendingPathComponent("assets") + guard let files = try? FileManager.default.contentsOfDirectory(at: assetsDirectory, includingPropertiesForKeys: nil), + let cssURL = files.first(where: { $0.lastPathComponent.hasPrefix("index-") && $0.lastPathComponent.hasSuffix(".css") }), + let css = try? String(contentsOf: cssURL, encoding: .utf8) else { + assertionFailure("Failed to load Gutenberg CSS from bundle") + return nil + } + return css + } +}