Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions Packages/CmuxFoundation/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// swift-tools-version: 6.0

import PackageDescription

let package = Package(
name: "CmuxFoundation",
platforms: [
.macOS(.v14),
],
products: [
.library(
name: "CmuxFoundation",
targets: ["CmuxFoundation"]
),
],
targets: [
.target(
name: "CmuxFoundation",
swiftSettings: [
.swiftLanguageMode(.v6),
.enableUpcomingFeature("ExistentialAny"),
.enableUpcomingFeature("InternalImportsByDefault"),
]
),
.testTarget(
name: "CmuxFoundationTests",
dependencies: ["CmuxFoundation"]
),
Comment on lines +25 to +28

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The test target does not carry the same swiftSettings as the library target. With swift-tools-version: 6.0, Swift 6 language mode is the default for all targets, so StrictConcurrency is already active — but ExistentialAny and InternalImportsByDefault are opt-in upcoming features and will not apply in the test target. Future tests that use bare existentials (Protocol instead of any Protocol) or rely on import-visibility semantics will silently diverge from production behavior. Adding the same settings block keeps the compilation environment uniform.

Suggested change
.testTarget(
name: "CmuxFoundationTests",
dependencies: ["CmuxFoundation"]
),
.testTarget(
name: "CmuxFoundationTests",
dependencies: ["CmuxFoundation"],
swiftSettings: [
.swiftLanguageMode(.v6),
.enableUpcomingFeature("ExistentialAny"),
.enableUpcomingFeature("InternalImportsByDefault"),
]
),

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

]
)
38 changes: 38 additions & 0 deletions Packages/CmuxFoundation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# CmuxFoundation

Shared low-level primitives for cmux with no internal package dependencies. This is the
bottom of the package dependency graph: encoding/text helpers, value types, and other
cross-cutting utilities that several domains need, with nothing in here depending on AppKit,
SwiftUI, or another cmux package.

It exists as the leaf every other package and the app target can depend on without creating
a cycle. Keep it dependency-free.

Foundation helpers are exposed as extensions on existing types rather than free functions,
so call sites read naturally (`value.javaScriptStringLiteral`, not `f(value)`).

## Contents

- `String.javaScriptStringLiteral` — the string encoded as a quoted JavaScript string literal.

## Usage

```swift
import CmuxFoundation

let literal = userText?.javaScriptStringLiteral ?? "null"
webView.evaluateJavaScript("setValue(\(literal))")
```

## Testing

Everything here is a pure value transform, so tests need no app, no AppKit, and no filesystem:

```swift
import Testing
import CmuxFoundation

@Test func plainStringIsQuoted() {
#expect("hello".javaScriptStringLiteral == "\"hello\"")
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Foundation

public extension String {
/// This string encoded as a quoted JavaScript string literal, ready to splice into JS source.
///
/// The value includes the surrounding double quotes and escapes any characters (quotes,
/// backslashes, control characters) that would otherwise break out of the literal. Encoding
/// goes through `JSONSerialization`, so the result is also a valid JSON string.
///
/// ```swift
/// "a\"b".javaScriptStringLiteral // -> "\"a\\\"b\""
/// webView.evaluateJavaScript("setValue(\(id?.javaScriptStringLiteral ?? "null"))")
/// ```
Comment on lines +10 to +13

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The doc-comment example uses id?.javaScriptStringLiteral ?? "null" where id: String?. Because javaScriptStringLiteral itself returns String?, optional chaining produces String??, and ?? unwraps only the outer level — the result is String?, not String. When that String? is interpolated into a JS template it renders as Optional(...), not the bare literal. The canonical call pattern should use flatMap to flatten the nested optional.

Suggested change
/// ```swift
/// "a\"b".javaScriptStringLiteral // -> "\"a\\\"b\""
/// webView.evaluateJavaScript("setValue(\(id?.javaScriptStringLiteral ?? "null"))")
/// ```
/// ```swift
/// "a\"b".javaScriptStringLiteral // -> "\"a\\\"b\""
/// webView.evaluateJavaScript("setValue(\(id.flatMap(\.javaScriptStringLiteral) ?? "null"))")
/// ```

///
/// - Returns: The quoted JS string literal, or `nil` if the string cannot be encoded.
var javaScriptStringLiteral: String? {
// [self] is always valid JSON; reuse JSON's escaping and drop the array brackets to
// get a quoted JS string literal.
guard let data = try? JSONSerialization.data(withJSONObject: [self]),
let arrayLiteral = String(data: data, encoding: .utf8),
arrayLiteral.count >= 2 else {
return nil
}
return String(arrayLiteral.dropFirst().dropLast())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Testing

import CmuxFoundation

@Suite struct StringJavaScriptStringLiteralTests {
@Test func plainStringIsQuoted() {
#expect("hello".javaScriptStringLiteral == "\"hello\"")
}

@Test func emptyStringIsEmptyQuotes() {
#expect("".javaScriptStringLiteral == "\"\"")
}

@Test func escapesDoubleQuotesAndBackslashes() {
#expect(#"a"b\c"#.javaScriptStringLiteral == #""a\"b\\c""#)
}

@Test func escapesNewlines() {
#expect("line1\nline2".javaScriptStringLiteral == #""line1\nline2""#)
}

@Test func preservesUnicode() {
#expect("café".javaScriptStringLiteral == "\"café\"")
}
}
14 changes: 2 additions & 12 deletions Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,7 @@ import WebKit
import Combine
import ObjectiveC.runtime
import Darwin

func cmuxJavaScriptStringLiteral(_ value: String?) -> String? {
guard let value else { return nil }
// Serialize as a JSON array, then strip the outer brackets to get a quoted JS string literal.
guard let data = try? JSONSerialization.data(withJSONObject: [value]),
let arrayLiteral = String(data: data, encoding: .utf8),
arrayLiteral.count >= 2 else {
return nil
}
return String(arrayLiteral.dropFirst().dropLast())
}
import CmuxFoundation

private struct MultiWindowRouteCLIResult {
let status: String
Expand Down Expand Up @@ -10524,7 +10514,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
awaitingInputId: String? = nil,
completion: @escaping ([String: String]) -> Void
) {
let expectedInputIdLiteral = cmuxJavaScriptStringLiteral(awaitingInputId) ?? "null"
let expectedInputIdLiteral = awaitingInputId?.javaScriptStringLiteral ?? "null"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Dangling call to removed free function — compilation error

The PR deletes cmuxJavaScriptStringLiteral(_:) from AppDelegate.swift and replaces it with the String.javaScriptStringLiteral property on CmuxFoundation, but this call site was never migrated. Because awaitingInputId is String?, the correct replacement is awaitingInputId?.javaScriptStringLiteral ?? "null". As written, the build will fail with "use of unresolved identifier 'cmuxJavaScriptStringLiteral'".

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Double-optional collapses to String?, not String, so \(expectedInputIdLiteral) in the template prints Optional("\"someId\"") rather than the bare literal. The JS engine receives const expectedInputId = Optional("\"someId\""); and throws a ReferenceError on every call where awaitingInputId is non-nil. Use flatMap to flatten the chain and get a plain String.

Suggested change
let expectedInputIdLiteral = awaitingInputId?.javaScriptStringLiteral ?? "null"
let expectedInputIdLiteral = awaitingInputId.flatMap(\.javaScriptStringLiteral) ?? "null"

let script = """
(() => {
const expectedInputId = \(expectedInputIdLiteral);
Expand Down
13 changes: 13 additions & 0 deletions cmux.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@
E7E000000000000000000001 /* CmuxEventStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7E000000000000000000002 /* CmuxEventStream.swift */; };
C0DE46000000000000000001 /* CMUXExtensionClient in Frameworks */ = {isa = PBXBuildFile; productRef = C0DE46000000000000000002 /* CMUXExtensionClient */; };
C0DE45000000000000000001 /* CmuxExtensionKit in Frameworks */ = {isa = PBXBuildFile; productRef = C0DE45000000000000000002 /* CmuxExtensionKit */; };
CF00F00000000000000000A3 /* CmuxFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = CF00F00000000000000000A2 /* CmuxFoundation */; };
C0DE34020000000000000001 /* CmuxHelpCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE34020000000000000003 /* CmuxHelpCommands.swift */; };
C0DE34020000000000000002 /* CmuxHelpResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE34020000000000000004 /* CmuxHelpResource.swift */; };
C0DE46010000000000000001 /* CMUXInstalledExtensionSidebarHostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE46010000000000000002 /* CMUXInstalledExtensionSidebarHostView.swift */; };
Expand Down Expand Up @@ -1216,6 +1217,7 @@
5EDB6027B346C46521A93C74 /* CMUXAuthCore in Frameworks */,
C0DE46000000000000000001 /* CMUXExtensionClient in Frameworks */,
C0DE45000000000000000001 /* CmuxExtensionKit in Frameworks */,
CF00F00000000000000000A3 /* CmuxFoundation in Frameworks */,
3069F1D10000000000000005 /* CMUXPasteboardFidelity in Frameworks */,
A5C0DE0000000000000000A3 /* CMUXProjectModel in Frameworks */,
CD0CFE5300000000CD0CFE53 /* CmuxSettings in Frameworks */,
Expand Down Expand Up @@ -1935,6 +1937,7 @@
A500D013A1B2C3D4E5F60718 /* CMUXDebugLog */,
CD0CFE5200000000CD0CFE52 /* CmuxSettings */,
CD0CFE5500000000CD0CFE55 /* CmuxSettingsUI */,
CF00F00000000000000000A2 /* CmuxFoundation */,
A8BD195031FC4B82B4354297 /* StackAuth */,
);
productName = cmux;
Expand Down Expand Up @@ -2072,6 +2075,7 @@
A5354304A5354304A5354304 /* XCLocalSwiftPackageReference "CMUXSocketPathDomain" */,
CD0CFE5100000000CD0CFE51 /* XCLocalSwiftPackageReference "CmuxSettings" */,
CD0CFE5400000000CD0CFE54 /* XCLocalSwiftPackageReference "CmuxSettingsUI" */,
CF00F00000000000000000A1 /* XCLocalSwiftPackageReference "CmuxFoundation" */,
28B798BB9086C8E6B60C3355 /* XCLocalSwiftPackageReference "stack-auth-swift-sdk-prerelease" */,
);
productRefGroup = A5001042 /* Products */;
Expand Down Expand Up @@ -3198,6 +3202,10 @@
isa = XCLocalSwiftPackageReference;
relativePath = Packages/CmuxSettingsUI;
};
CF00F00000000000000000A1 /* XCLocalSwiftPackageReference "CmuxFoundation" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Packages/CmuxFoundation;
};
A5001260 /* XCLocalSwiftPackageReference "bonsplit" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = vendor/bonsplit;
Expand Down Expand Up @@ -3300,6 +3308,11 @@
package = CD0CFE5400000000CD0CFE54 /* XCLocalSwiftPackageReference "CmuxSettingsUI" */;
productName = CmuxSettingsUI;
};
CF00F00000000000000000A2 /* CmuxFoundation */ = {
isa = XCSwiftPackageProductDependency;
package = CF00F00000000000000000A1 /* XCLocalSwiftPackageReference "CmuxFoundation" */;
productName = CmuxFoundation;
};
A5001231 /* Sparkle */ = {
isa = XCSwiftPackageProductDependency;
package = A5001232 /* XCRemoteSwiftPackageReference "Sparkle" */;
Expand Down
3 changes: 3 additions & 0 deletions cmux.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading