Add Xcode build phase for web frontend

- Added "Build Web Frontend" phase that runs npm install and npm run build
- Copies built web assets from web/public/ to app Resources folder
- Modified TunnelServer.swift to only serve static files from bundled Resources
- Removed development path options from static file serving
- Added proper PATH configuration for npm in build script
This commit is contained in:
Peter Steinberger 2025-06-16 06:54:04 +02:00
parent 3a6665c13f
commit 91f12f6ed6
2 changed files with 45 additions and 45 deletions

View file

@ -125,6 +125,7 @@
788687ED2DFF4FCB00B22C15 /* Sources */,
788687EE2DFF4FCB00B22C15 /* Frameworks */,
788687EF2DFF4FCB00B22C15 /* Resources */,
B2C3D4E5F6A7B8C9D0E1F234 /* Build Web Frontend */,
A189466CB0AD49BEBE16B954 /* Build tty-fwd Universal Binary */,
);
buildRules = (
@ -262,6 +263,31 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
B2C3D4E5F6A7B8C9D0E1F234 /* Build Web Frontend */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"$(SRCROOT)/web/package.json",
"$(SRCROOT)/web/src",
"$(SRCROOT)/web/tsconfig.json",
"$(SRCROOT)/web/tsconfig.client.json",
"$(SRCROOT)/web/tailwind.config.js",
);
name = "Build Web Frontend";
outputFileListPaths = (
);
outputPaths = (
"$(BUILT_PRODUCTS_DIR)/$(CONTENTS_FOLDER_PATH)/Resources/web/public",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# Build web frontend\necho \"Building web frontend...\"\n\n# Setup PATH to include common Node.js installation locations\nexport PATH=\"/usr/local/bin:/opt/homebrew/bin:/opt/homebrew/sbin:$HOME/.nvm/versions/node/$(ls -1 $HOME/.nvm/versions/node 2>/dev/null | tail -1)/bin:$PATH\"\n\n# Get the project directory\nPROJECT_DIR=\"${SRCROOT}\"\nWEB_DIR=\"${PROJECT_DIR}/web\"\nPUBLIC_DIR=\"${WEB_DIR}/public\"\nDEST_DIR=\"${BUILT_PRODUCTS_DIR}/${CONTENTS_FOLDER_PATH}/Resources/web/public\"\n\n# Export CI environment variable to prevent interactive prompts\nexport CI=true\n\n# Check if npm is available\nif ! command -v npm &> /dev/null; then\n echo \"error: npm could not be found in PATH\"\n echo \"PATH is: $PATH\"\n echo \"Please ensure Node.js is installed\"\n exit 1\nfi\n\n# Print npm version for debugging\necho \"Using npm version: $(npm --version)\"\necho \"Using node version: $(node --version)\"\n\n# Check if web directory exists\nif [ ! -d \"${WEB_DIR}\" ]; then\n echo \"error: Web directory not found at ${WEB_DIR}\"\n exit 1\nfi\n\n# Change to web directory\ncd \"${WEB_DIR}\"\n\n# Install dependencies if node_modules doesn't exist or package-lock.json is newer\nif [ ! -d \"node_modules\" ] || [ \"package-lock.json\" -nt \"node_modules\" ]; then\n echo \"Installing npm dependencies...\"\n npm install --no-progress --no-audit\n if [ $? -ne 0 ]; then\n echo \"error: npm install failed\"\n exit 1\n fi\nfi\n\n# Build the web frontend\necho \"Running npm build...\"\nnpm run build\nif [ $? -ne 0 ]; then\n echo \"error: npm run build failed\"\n exit 1\nfi\n\n# Create destination directory\nmkdir -p \"${DEST_DIR}\"\n\n# Copy built files to Resources\necho \"Copying web files to app bundle...\"\nif [ -d \"${PUBLIC_DIR}\" ]; then\n # Copy all files from public directory\n cp -R \"${PUBLIC_DIR}/\"* \"${DEST_DIR}/\"\n echo \"Web frontend files copied to ${DEST_DIR}\"\nelse\n echo \"error: Public directory not found at ${PUBLIC_DIR}\"\n exit 1\nfi\n";
};
A189466CB0AD49BEBE16B954 /* Build tty-fwd Universal Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;

View file

@ -474,54 +474,28 @@ public final class TunnelServer {
// MARK: - Static File Serving
private func serveStaticFile(path: String) async -> Response {
// Try multiple possible paths for the web/public directory
let possiblePaths = [
// Bundle resource path (for production app)
Bundle.main.resourcePath?.appending("/web/public"),
// Current working directory (for development)
FileManager.default.currentDirectoryPath + "/web/public",
// Project directory (if running from source)
"/Users/mitsuhiko/Development/vibetunnel/web/public",
// Relative to bundle path
Bundle.main.bundlePath + "/../../../web/public"
].compactMap(\.self)
// Serve files only from the bundled Resources folder
guard let resourcePath = Bundle.main.resourcePath else {
logger.error("Bundle resource path not found")
return errorResponse(message: "Resource bundle not available", status: .internalServerError)
}
let webPublicPath = resourcePath + "/web/public"
// Sanitize path to prevent directory traversal attacks
let sanitizedPath = path.replacingOccurrences(of: "..", with: "")
var webPublicPath: String?
var fullPath: String?
// Find the first path that exists
for testPath in possiblePaths {
let testFullPath = testPath + "/" + sanitizedPath
if FileManager.default.fileExists(atPath: testFullPath) {
webPublicPath = testPath
fullPath = testFullPath
break
}
}
// If no file found, try just checking directory existence
if fullPath == nil {
for testPath in possiblePaths {
if FileManager.default.fileExists(atPath: testPath, isDirectory: nil) {
webPublicPath = testPath
fullPath = testPath + "/" + sanitizedPath
break
}
}
}
guard let finalPath = fullPath, webPublicPath != nil else {
logger.error("Could not find web/public directory in any of these paths:")
for testPath in possiblePaths {
logger.error(" - \(testPath)")
}
return errorResponse(message: "Web directory not found", status: .notFound)
let fullPath = webPublicPath + "/" + sanitizedPath
// Check if the web directory exists in Resources
var isWebDirExists: ObjCBool = false
if !FileManager.default.fileExists(atPath: webPublicPath, isDirectory: &isWebDirExists) || !isWebDirExists.boolValue {
logger.error("Web resources not found at: \(webPublicPath)")
logger.error("Make sure the app was built with the 'Build Web Frontend' phase")
return errorResponse(message: "Web resources not bundled", status: .internalServerError)
}
var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: finalPath, isDirectory: &isDirectory) else {
guard FileManager.default.fileExists(atPath: fullPath, isDirectory: &isDirectory) else {
return errorResponse(message: "File not found", status: .notFound)
}
@ -531,7 +505,7 @@ public final class TunnelServer {
}
do {
let fileData = try Data(contentsOf: URL(fileURLWithPath: finalPath))
let fileData = try Data(contentsOf: URL(fileURLWithPath: fullPath))
var buffer = ByteBuffer()
buffer.writeBytes(fileData)