react-native-control-center 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +415 -0
  3. package/app.plugin.js +14 -0
  4. package/cli/bin/rn-control-center.js +52 -0
  5. package/ios/ControlStoreRuntime.swift +81 -0
  6. package/ios/RNControlCenter.mm +28 -0
  7. package/ios/RNControlCenter.swift +195 -0
  8. package/lib/commonjs/cli/runGenerate.d.ts +22 -0
  9. package/lib/commonjs/cli/runGenerate.js +173 -0
  10. package/lib/commonjs/core/generate/entitlements.d.ts +17 -0
  11. package/lib/commonjs/core/generate/entitlements.js +31 -0
  12. package/lib/commonjs/core/generate/index.d.ts +30 -0
  13. package/lib/commonjs/core/generate/index.js +58 -0
  14. package/lib/commonjs/core/generate/plist.d.ts +13 -0
  15. package/lib/commonjs/core/generate/plist.js +37 -0
  16. package/lib/commonjs/core/generate/swift.d.ts +22 -0
  17. package/lib/commonjs/core/generate/swift.js +140 -0
  18. package/lib/commonjs/core/parseControls.d.ts +9 -0
  19. package/lib/commonjs/core/parseControls.js +206 -0
  20. package/lib/commonjs/core/sf-symbols-data.d.ts +3 -0
  21. package/lib/commonjs/core/sf-symbols-data.js +5373 -0
  22. package/lib/commonjs/core/templates/ButtonControl.swift.hbs +28 -0
  23. package/lib/commonjs/core/templates/ButtonIntent.swift.hbs +39 -0
  24. package/lib/commonjs/core/templates/ControlBundle.swift.hbs +17 -0
  25. package/lib/commonjs/core/templates/ControlStore.swift.hbs +149 -0
  26. package/lib/commonjs/core/templates/ToggleControl.swift.hbs +60 -0
  27. package/lib/commonjs/core/templates/ToggleIntent.swift.hbs +49 -0
  28. package/lib/commonjs/core/types.d.ts +14 -0
  29. package/lib/commonjs/core/types.js +17 -0
  30. package/lib/commonjs/core/validateSymbols.d.ts +15 -0
  31. package/lib/commonjs/core/validateSymbols.js +43 -0
  32. package/lib/commonjs/core/xcode/addSyncedFolder.d.ts +28 -0
  33. package/lib/commonjs/core/xcode/addSyncedFolder.js +71 -0
  34. package/lib/commonjs/core/xcode/addTarget.d.ts +25 -0
  35. package/lib/commonjs/core/xcode/addTarget.js +34 -0
  36. package/lib/commonjs/core/xcode/buildSettings.d.ts +14 -0
  37. package/lib/commonjs/core/xcode/buildSettings.js +57 -0
  38. package/lib/commonjs/core/xcode/embed.d.ts +16 -0
  39. package/lib/commonjs/core/xcode/embed.js +74 -0
  40. package/lib/commonjs/core/xcode/inspect.d.ts +29 -0
  41. package/lib/commonjs/core/xcode/inspect.js +87 -0
  42. package/lib/commonjs/core/xcode/linkFrameworks.d.ts +18 -0
  43. package/lib/commonjs/core/xcode/linkFrameworks.js +80 -0
  44. package/lib/commonjs/core/xcode/types.d.ts +121 -0
  45. package/lib/commonjs/core/xcode/types.js +7 -0
  46. package/lib/commonjs/core/xcode/wire.d.ts +27 -0
  47. package/lib/commonjs/core/xcode/wire.js +142 -0
  48. package/lib/commonjs/plugin/index.d.ts +43 -0
  49. package/lib/commonjs/plugin/index.js +177 -0
  50. package/lib/commonjs/src/ControlCenter.d.ts +34 -0
  51. package/lib/commonjs/src/ControlCenter.js +91 -0
  52. package/lib/commonjs/src/defineControls.d.ts +6 -0
  53. package/lib/commonjs/src/defineControls.js +10 -0
  54. package/lib/commonjs/src/hooks.d.ts +8 -0
  55. package/lib/commonjs/src/hooks.js +38 -0
  56. package/lib/commonjs/src/index.d.ts +5 -0
  57. package/lib/commonjs/src/index.js +9 -0
  58. package/lib/commonjs/src/sf-symbols.d.ts +8 -0
  59. package/lib/commonjs/src/sf-symbols.js +2 -0
  60. package/lib/commonjs/src/stateCache.d.ts +8 -0
  61. package/lib/commonjs/src/stateCache.js +36 -0
  62. package/lib/commonjs/src/types.d.ts +36 -0
  63. package/lib/commonjs/src/types.js +2 -0
  64. package/package.json +75 -0
  65. package/src/ControlCenter.ts +122 -0
  66. package/src/defineControls.ts +9 -0
  67. package/src/hooks.ts +42 -0
  68. package/src/index.ts +12 -0
  69. package/src/sf-symbols.ts +251 -0
  70. package/src/stateCache.ts +34 -0
  71. package/src/types.ts +36 -0
@@ -0,0 +1,28 @@
1
+ import AppIntents
2
+ import SwiftUI
3
+ import WidgetKit
4
+
5
+ // ─────────────────────────────────────────────────────────────────────────
6
+ // 📄 {{pascalCase id}}Control.swift (Controls/{{pascalCase id}}Control.swift)
7
+ // Button 컨트롤의 표시 정의 — 제어센터에 어떤 모양으로 보일지
8
+ // (탭했을 때 일어나는 일은 Intents/{{pascalCase id}}Intent.swift 참조)
9
+ // ─────────────────────────────────────────────────────────────────────────
10
+
11
+ struct {{pascalCase id}}Control: ControlWidget {
12
+ var body: some ControlWidgetConfiguration {
13
+ StaticControlConfiguration(
14
+ kind: "{{bundleId}}.{{id}}"
15
+ ) {
16
+ ControlWidgetButton(action: {{pascalCase id}}Intent()) {
17
+ Label("{{title}}", systemImage: "{{icon}}")
18
+ {{#if tint}}
19
+ .tint(Color(hex: "{{tint}}"))
20
+ {{/if}}
21
+ }
22
+ }
23
+ .displayName("{{title}}")
24
+ {{#if description}}
25
+ .description("{{description}}")
26
+ {{/if}}
27
+ }
28
+ }
@@ -0,0 +1,39 @@
1
+ import AppIntents
2
+
3
+ // ─────────────────────────────────────────────────────────────────────────
4
+ // 📄 {{pascalCase id}}Intent.swift (Intents/{{pascalCase id}}Intent.swift)
5
+ // Button 탭 → 앱 열림까지의 흐름
6
+ // ─────────────────────────────────────────────────────────────────────────
7
+ //
8
+ // ① 사용자가 제어센터에서 "{{title}}" 버튼을 탭함
9
+ // ↓
10
+ // ② iOS가 위젯 익스텐션 프로세스를 깨워서 perform() 호출
11
+ // ↓
12
+ // ③ perform() 안에서:
13
+ // (a) ControlStore.shared.enqueueAction() 호출
14
+ // → App Group UserDefaults 큐에 이벤트 저장 (영구)
15
+ // → Darwin Notification 발송 (앱이 살아있으면 즉시 알림)
16
+ // (b) return .result()
17
+ // ↓
18
+ // ④ openAppWhenRun = true 이므로 iOS가 메인 앱을 포어그라운드로 띄움
19
+ // ↓
20
+ // ⑤ 메인 앱 시작/복귀 후 Native Module이 큐를 drain (Week 5에서 구현)
21
+ // ↓
22
+ // ⑥ JS의 ControlCenter.onAction((id) => { ... }) 콜백 발화
23
+ //
24
+ // ─────────────────────────────────────────────────────────────────────────
25
+
26
+ struct {{pascalCase id}}Intent: AppIntent {
27
+ static let title: LocalizedStringResource = "{{title}}"
28
+ static let openAppWhenRun: Bool = true // ④ 이 플래그가 메인 앱을 깨움
29
+
30
+ func perform() async throws -> some IntentResult {
31
+ // ③ (a): App Group 큐에 이벤트 기록 + Darwin 알림 발송
32
+ ControlStore.shared.enqueueAction(
33
+ id: "{{id}}",
34
+ deepLink: "{{deepLink}}"
35
+ )
36
+ // ③ (b): perform 종료. 이후 ④(앱 열기)는 iOS가 자동 처리
37
+ return .result()
38
+ }
39
+ }
@@ -0,0 +1,17 @@
1
+ import WidgetKit
2
+ import SwiftUI
3
+
4
+ // ─────────────────────────────────────────────────────────────────────────
5
+ // 📄 ControlBundle.swift
6
+ // 위젯 익스텐션 진입점 — iOS가 이 @main 구조체를 통해 모든 컨트롤을 인식.
7
+ // controls.ts에 선언된 모든 컨트롤이 여기 등록됨.
8
+ // ─────────────────────────────────────────────────────────────────────────
9
+
10
+ @main
11
+ struct {{bundleStructName}}: WidgetBundle {
12
+ var body: some Widget {
13
+ {{#each controls}}
14
+ {{pascalCase id}}Control()
15
+ {{/each}}
16
+ }
17
+ }
@@ -0,0 +1,149 @@
1
+ import Foundation
2
+
3
+ // ─────────────────────────────────────────────────────────────────────────
4
+ // 📄 ControlStore.swift (위젯 + 메인 앱 양쪽에서 공유)
5
+ // 위젯 익스텐션과 메인 앱 사이의 다리
6
+ // ─────────────────────────────────────────────────────────────────────────
7
+ //
8
+ // 위젯과 앱은 서로 다른 프로세스(메모리 분리)이므로 변수로는 정보 공유 불가.
9
+ // 대신 두 채널을 사용한다:
10
+ //
11
+ // 1. App Group UserDefaults
12
+ // - 같은 그룹 ID를 양쪽 entitlement에 등록한 두 프로세스가 공유하는
13
+ // 영구 키-값 저장소
14
+ // - 데이터를 "남겨두는" 역할 (앱이 죽었다가 다시 살아도 살아남음)
15
+ //
16
+ // 2. Darwin Notification
17
+ // - 다윈 커널 수준의 글로벌 신호 (페이로드 없음)
18
+ // - "값이 바뀌었음!" 이라는 신호만 전달, 받은 쪽이 ①에서 새 값을 읽음
19
+ // - 앱이 살아있을 때만 의미 있음 (앱이 죽어있으면 신호 사라짐)
20
+ //
21
+ // 두 채널을 합치면: 영구 저장 + 즉시 알림 둘 다 충족.
22
+ //
23
+ // 자동 생성됨 — 직접 편집하지 마세요. 다음 prebuild 때 덮어써집니다.
24
+ // ─────────────────────────────────────────────────────────────────────────
25
+
26
+ public final class ControlStore {
27
+ public static let shared = ControlStore()
28
+
29
+ // 사용자 라이브러리 설정에서 주입된 값
30
+ public static let appGroupId = "{{appGroupId}}"
31
+ public static let darwinNotificationName = "{{appGroupId}}.event"
32
+
33
+ // 큐 저장 키 — 두 종류의 이벤트를 분리 저장
34
+ private static let actionQueueKey = "__rncc.actionQueue" // Button 탭 이벤트
35
+ private static let stateChangeQueueKey = "__rncc.stateChangeQueue" // Toggle 변경 이벤트
36
+
37
+ private let defaults: UserDefaults
38
+
39
+ private init() {
40
+ // App Group UserDefaults 열기. 이게 실패하면 entitlement 미설정 의심.
41
+ guard let groupDefaults = UserDefaults(suiteName: ControlStore.appGroupId) else {
42
+ preconditionFailure(
43
+ "ControlStore: failed to open App Group UserDefaults for \(ControlStore.appGroupId). " +
44
+ "Verify the App Group entitlement is configured for both targets."
45
+ )
46
+ }
47
+ self.defaults = groupDefaults
48
+ }
49
+
50
+ // ─── Toggle 상태 R/W ─────────────────────────────────────────────────
51
+ // Provider.currentValue() → getBool() (위젯이 "지금 ON?" 물어볼 때)
52
+ // Toggle Intent.perform() → setBool() (사용자가 토글 누를 때)
53
+ // 메인 앱의 useControlState 훅 → 양쪽 다 사용 (Week 5)
54
+
55
+ public func getBool(_ key: String) -> Bool {
56
+ return defaults.bool(forKey: key)
57
+ }
58
+
59
+ public func setBool(_ key: String, value: Bool) {
60
+ defaults.set(value, forKey: key)
61
+ }
62
+
63
+ public func getValue<T>(_ key: String, default defaultValue: T) -> T {
64
+ return (defaults.object(forKey: key) as? T) ?? defaultValue
65
+ }
66
+
67
+ public func setValue(_ key: String, value: Any) {
68
+ defaults.set(value, forKey: key)
69
+ }
70
+
71
+ /// 라이브러리가 관리하는 toggle stateKey 목록 (codegen이 정적으로 주입).
72
+ /// dictionaryRepresentation()은 시스템 전역 도메인까지 섞여 나오므로
73
+ /// 우리가 선언한 키만 정확히 추리기 위해 이 화이트리스트를 쓴다.
74
+ public static let stateKeys: [String] = [{{#each stateKeys}}"{{this}}"{{#unless @last}}, {{/unless}}{{/each}}]
75
+
76
+ /// 선언된 stateKey들의 현재 값 스냅샷.
77
+ /// Native Module이 모듈 등록 시점에 JS로 넘겨, 콜드 스타트 직후에도
78
+ /// useControlState 첫 렌더에서 값을 동기로 쓸 수 있게 한다. (Week 6)
79
+ public func snapshot() -> [String: Any] {
80
+ var out: [String: Any] = [:]
81
+ for key in Self.stateKeys {
82
+ if let value = defaults.object(forKey: key) {
83
+ out[key] = value
84
+ }
85
+ }
86
+ return out
87
+ }
88
+
89
+ // ─── 이벤트 큐 (Widget → App) ────────────────────────────────────────
90
+ // Button.perform()에서 enqueueAction()을 호출하면
91
+ // (1) UserDefaults 큐에 이벤트 push
92
+ // (2) Darwin notification 발송
93
+ // 메인 앱의 Native Module이 이 큐를 drain → JS 콜백 발화 (Week 5)
94
+
95
+ /// Button intent에서 호출.
96
+ func enqueueAction(id: String, deepLink: String) {
97
+ appendToQueue(
98
+ key: Self.actionQueueKey,
99
+ event: [
100
+ "id": id,
101
+ "deepLink": deepLink,
102
+ "t": Date().timeIntervalSince1970,
103
+ ]
104
+ )
105
+ postDarwinNotification()
106
+ }
107
+
108
+ /// Toggle intent에서 호출.
109
+ func enqueueStateChange(key: String, value: Any) {
110
+ appendToQueue(
111
+ key: Self.stateChangeQueueKey,
112
+ event: [
113
+ "key": key,
114
+ "value": value,
115
+ "t": Date().timeIntervalSince1970,
116
+ ]
117
+ )
118
+ postDarwinNotification()
119
+ }
120
+
121
+ /// 메인 앱의 Native Module이 호출 — 큐를 비우고 이벤트 배열 반환.
122
+ public func dequeueActionEvents() -> [[String: Any]] {
123
+ let events = (defaults.array(forKey: Self.actionQueueKey) as? [[String: Any]]) ?? []
124
+ defaults.removeObject(forKey: Self.actionQueueKey)
125
+ return events
126
+ }
127
+
128
+ public func dequeueStateChangeEvents() -> [[String: Any]] {
129
+ let events = (defaults.array(forKey: Self.stateChangeQueueKey) as? [[String: Any]]) ?? []
130
+ defaults.removeObject(forKey: Self.stateChangeQueueKey)
131
+ return events
132
+ }
133
+
134
+ // ─── 내부 ────────────────────────────────────────────────────────────
135
+
136
+ private func appendToQueue(key: String, event: [String: Any]) {
137
+ var queue = (defaults.array(forKey: key) as? [[String: Any]]) ?? []
138
+ queue.append(event)
139
+ defaults.set(queue, forKey: key)
140
+ }
141
+
142
+ /// Darwin notification 발송. Cross-process 즉시 신호.
143
+ /// 페이로드는 없고 "뭔가 바뀜!" 신호만 전달.
144
+ private func postDarwinNotification() {
145
+ let center = CFNotificationCenterGetDarwinNotifyCenter()
146
+ let name = CFNotificationName(Self.darwinNotificationName as CFString)
147
+ CFNotificationCenterPostNotification(center, name, nil, nil, true)
148
+ }
149
+ }
@@ -0,0 +1,60 @@
1
+ import AppIntents
2
+ import SwiftUI
3
+ import WidgetKit
4
+
5
+ // ─────────────────────────────────────────────────────────────────────────
6
+ // 📄 {{pascalCase id}}Control.swift (Controls/{{pascalCase id}}Control.swift)
7
+ // Toggle 표시 흐름 (제어센터를 열 때마다)
8
+ // ─────────────────────────────────────────────────────────────────────────
9
+ //
10
+ // ① 사용자가 제어센터를 열거나 토글이 화면에 보이려 함
11
+ // ↓
12
+ // ② iOS가 Provider.currentValue() 호출 → "지금 ON 인지 OFF 인지?" 질의
13
+ // ↓
14
+ // ③ Provider가 ControlStore.shared.getBool("{{stateKey}}") 호출
15
+ // → App Group UserDefaults에서 현재 값 읽음
16
+ // ↓
17
+ // ④ iOS가 그 값(isOn)으로 ControlWidgetToggle을 그림
18
+ // - on이면 icons.on, off면 icons.off
19
+ // - on/off 색상도 분기
20
+ //
21
+ // ─────────────────────────────────────────────────────────────────────────
22
+
23
+ struct {{pascalCase id}}Control: ControlWidget {
24
+ var body: some ControlWidgetConfiguration {
25
+ StaticControlConfiguration(
26
+ kind: "{{bundleId}}.{{id}}",
27
+ provider: {{pascalCase id}}Provider() // ② 상태 질의 대상
28
+ ) { isOn in
29
+ ControlWidgetToggle(
30
+ "{{title}}",
31
+ isOn: isOn, // ④ Provider가 답한 값
32
+ action: {{pascalCase id}}Intent() // 사용자가 탭하면 이게 실행
33
+ ) { isOn in
34
+ Label(
35
+ "{{title}}",
36
+ systemImage: isOn ? "{{icons.on}}" : "{{icons.off}}"
37
+ )
38
+ {{#if tint}}
39
+ .tint(Color(hex: isOn ? "{{tint.on}}" : "{{tint.off}}"))
40
+ {{/if}}
41
+ }
42
+ }
43
+ .displayName("{{title}}")
44
+ {{#if description}}
45
+ .description("{{description}}")
46
+ {{/if}}
47
+ }
48
+ }
49
+
50
+ extension {{pascalCase id}}Control {
51
+ struct {{pascalCase id}}Provider: ControlValueProvider {
52
+ // 미리보기/플레이스홀더용 기본값
53
+ var previewValue: Bool { false }
54
+
55
+ // ③ iOS가 토글을 그릴 때마다 호출. 공유 저장소에서 현재 상태를 읽어 반환.
56
+ func currentValue() async throws -> Bool {
57
+ return ControlStore.shared.getBool("{{stateKey}}")
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,49 @@
1
+ import AppIntents
2
+
3
+ // ─────────────────────────────────────────────────────────────────────────
4
+ // 📄 {{pascalCase id}}Intent.swift (Intents/{{pascalCase id}}Intent.swift)
5
+ // Toggle 탭 → 상태 변경 흐름
6
+ // ─────────────────────────────────────────────────────────────────────────
7
+ //
8
+ // ① 사용자가 제어센터에서 "{{title}}" 토글을 탭함
9
+ // ↓
10
+ // ② iOS가 새 값을 결정 (현재 OFF면 true, ON이면 false)
11
+ // ↓
12
+ // ③ iOS가 {{pascalCase id}}Intent를 인스턴스화하고 self.value에 새 값 주입
13
+ // ↓
14
+ // ④ perform() 호출:
15
+ // (a) ControlStore.shared.setBool("{{stateKey}}", value)
16
+ // → App Group UserDefaults에 새 상태 저장 (먼저!)
17
+ // (b) ControlStore.shared.enqueueStateChange()
18
+ // → JS에 알릴 이벤트 큐에 기록
19
+ // → Darwin Notification 발송 (앱이 살아있으면 즉시 깨움)
20
+ // ↓
21
+ // ⑤ return .result() — perform 종료
22
+ // ↓
23
+ // ⑥ iOS가 토글을 새 값으로 다시 그림 (Provider 재호출)
24
+ // ↓
25
+ // ⑦ (앱이 실행 중이면) Native Module이 ④(b) 큐 drain → JS의
26
+ // ControlCenter.onStateChange("{{stateKey}}", v => ...) 콜백 발화 (Week 5)
27
+ //
28
+ // ─────────────────────────────────────────────────────────────────────────
29
+
30
+ struct {{pascalCase id}}Intent: SetValueIntent {
31
+ static let title: LocalizedStringResource = "{{title}}"
32
+
33
+ @Parameter(title: "{{title}}")
34
+ var value: Bool // ③ iOS가 자동 주입
35
+
36
+ func perform() async throws -> some IntentResult {
37
+ // ④ (a): 새 상태를 공유 저장소에 먼저 저장 (순서 중요)
38
+ ControlStore.shared.setBool("{{stateKey}}", value: value)
39
+
40
+ // ④ (b): JS 측에 변화를 알릴 이벤트 큐에 기록 + Darwin 알림 발송
41
+ ControlStore.shared.enqueueStateChange(
42
+ key: "{{stateKey}}",
43
+ value: value
44
+ )
45
+
46
+ // ⑤ 끝
47
+ return .result()
48
+ }
49
+ }
@@ -0,0 +1,14 @@
1
+ import type { ButtonControl, ToggleControl } from '../src/types';
2
+ export type ParsedButtonControl = ButtonControl & {
3
+ id: string;
4
+ };
5
+ export type ParsedToggleControl = ToggleControl & {
6
+ id: string;
7
+ };
8
+ export type ParsedControl = ParsedButtonControl | ParsedToggleControl;
9
+ export declare class ParseError extends Error {
10
+ readonly filePath: string;
11
+ readonly line?: number | undefined;
12
+ readonly column?: number | undefined;
13
+ constructor(message: string, filePath: string, line?: number | undefined, column?: number | undefined);
14
+ }
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ParseError = void 0;
4
+ class ParseError extends Error {
5
+ filePath;
6
+ line;
7
+ column;
8
+ constructor(message, filePath, line, column) {
9
+ const loc = line !== undefined ? ` (${filePath}:${line}:${column ?? 0})` : ` (${filePath})`;
10
+ super(`${message}${loc}`);
11
+ this.filePath = filePath;
12
+ this.line = line;
13
+ this.column = column;
14
+ this.name = 'ParseError';
15
+ }
16
+ }
17
+ exports.ParseError = ParseError;
@@ -0,0 +1,15 @@
1
+ import type { ParsedControl } from './types';
2
+ export interface UnknownSymbol {
3
+ controlId: string;
4
+ field: string;
5
+ name: string;
6
+ }
7
+ /** 알려진 SF Symbol인지 검사. */
8
+ export declare function isKnownSymbol(name: string): boolean;
9
+ /** 컨트롤 목록에서 알 수 없는 심볼 참조를 모두 수집. */
10
+ export declare function collectUnknownSymbols(controls: ParsedControl[]): UnknownSymbol[];
11
+ /**
12
+ * 알 수 없는 심볼을 console.warn으로 보고. (plugin/CLI가 빌드 중 호출)
13
+ * 하나도 없으면 아무것도 출력하지 않는다.
14
+ */
15
+ export declare function warnUnknownSymbols(controls: ParsedControl[]): UnknownSymbol[];
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isKnownSymbol = isKnownSymbol;
4
+ exports.collectUnknownSymbols = collectUnknownSymbols;
5
+ exports.warnUnknownSymbols = warnUnknownSymbols;
6
+ const sf_symbols_data_1 = require("./sf-symbols-data");
7
+ /** 알려진 SF Symbol인지 검사. */
8
+ function isKnownSymbol(name) {
9
+ return sf_symbols_data_1.SF_SYMBOLS.has(name);
10
+ }
11
+ /** 컨트롤 목록에서 알 수 없는 심볼 참조를 모두 수집. */
12
+ function collectUnknownSymbols(controls) {
13
+ const unknown = [];
14
+ for (const control of controls) {
15
+ if (control.type === 'button') {
16
+ if (!isKnownSymbol(control.icon)) {
17
+ unknown.push({ controlId: control.id, field: 'icon', name: control.icon });
18
+ }
19
+ }
20
+ else if (control.type === 'toggle') {
21
+ if (!isKnownSymbol(control.icons.on)) {
22
+ unknown.push({ controlId: control.id, field: 'icons.on', name: control.icons.on });
23
+ }
24
+ if (!isKnownSymbol(control.icons.off)) {
25
+ unknown.push({ controlId: control.id, field: 'icons.off', name: control.icons.off });
26
+ }
27
+ }
28
+ }
29
+ return unknown;
30
+ }
31
+ /**
32
+ * 알 수 없는 심볼을 console.warn으로 보고. (plugin/CLI가 빌드 중 호출)
33
+ * 하나도 없으면 아무것도 출력하지 않는다.
34
+ */
35
+ function warnUnknownSymbols(controls) {
36
+ const unknown = collectUnknownSymbols(controls);
37
+ for (const u of unknown) {
38
+ console.warn(`[react-native-control-center] Control "${u.controlId}" uses "${u.name}" ` +
39
+ `for ${u.field}, which is not a known SF Symbol. ` +
40
+ `Check the spelling in SF Symbols.app — if it's a newer symbol this warning is safe to ignore.`);
41
+ }
42
+ return unknown;
43
+ }
@@ -0,0 +1,28 @@
1
+ import type { PBXProject } from 'xcode';
2
+ import './types';
3
+ export interface AddSyncedFolderOptions {
4
+ /** 위젯 타겟 uuid — 이 폴더의 1차 멤버 */
5
+ widgetTargetUuid: string;
6
+ /** 메인 앱 타겟 uuid — sharedFiles에 한해 추가 멤버 */
7
+ mainAppTargetUuid: string;
8
+ /** 폴더 이름 (사용자 ios/ 디렉토리 기준 상대 경로) */
9
+ folderName: string;
10
+ /** 메인 앱과 공유할 폴더 내 파일들 (폴더 기준 상대 경로) */
11
+ sharedFiles: string[];
12
+ /**
13
+ * 위젯 타겟의 Sources/Resources 자동 멤버십에서 제외할 파일들.
14
+ * Info.plist, *.entitlements 같이 build setting으로 따로 참조하는 파일은
15
+ * 자동 컴파일/복사에서 빼야 "Multiple commands produce" 충돌을 피한다.
16
+ */
17
+ excludedFromWidget?: string[];
18
+ }
19
+ /**
20
+ * Xcode 16+의 fileSystemSynchronizedGroups 기반 멤버십 처리.
21
+ *
22
+ * 동작:
23
+ * 1) PBXFileSystemSynchronizedRootGroup을 만들어 폴더와 위젯 타겟을 매핑
24
+ * 2) sharedFiles가 있으면 ExceptionSet을 만들어 메인 앱 타겟에 그 파일들을 추가 멤버십으로 부여
25
+ *
26
+ * xcode npm 패키지(@3.0.1)는 이 객체들을 모르므로 pbxproj 섹션을 직접 변형한다.
27
+ */
28
+ export declare function addSyncedSourceFolder(project: PBXProject, options: AddSyncedFolderOptions): void;
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.addSyncedSourceFolder = addSyncedSourceFolder;
4
+ require("./types");
5
+ /**
6
+ * Xcode 16+의 fileSystemSynchronizedGroups 기반 멤버십 처리.
7
+ *
8
+ * 동작:
9
+ * 1) PBXFileSystemSynchronizedRootGroup을 만들어 폴더와 위젯 타겟을 매핑
10
+ * 2) sharedFiles가 있으면 ExceptionSet을 만들어 메인 앱 타겟에 그 파일들을 추가 멤버십으로 부여
11
+ *
12
+ * xcode npm 패키지(@3.0.1)는 이 객체들을 모르므로 pbxproj 섹션을 직접 변형한다.
13
+ */
14
+ function addSyncedSourceFolder(project, options) {
15
+ const objects = project.hash.project.objects;
16
+ // 0) ExceptionSet들 만들기. 다음 두 종류:
17
+ // - sharedFiles → 메인 앱이 추가 멤버로 가져갈 파일들
18
+ // - excludedFromWidget → 위젯 자기 자신의 자동 멤버십에서 빼는 파일들 (Info.plist 등)
19
+ const exceptionRefs = [];
20
+ const exceptionSection = (objects['PBXFileSystemSynchronizedBuildFileExceptionSet'] ??= {});
21
+ if (options.sharedFiles.length > 0) {
22
+ const uuid = generateUuid(project);
23
+ const comment = `Exceptions for "${options.folderName}" folder in main app target`;
24
+ exceptionSection[uuid] = {
25
+ isa: 'PBXFileSystemSynchronizedBuildFileExceptionSet',
26
+ membershipExceptions: options.sharedFiles,
27
+ target: options.mainAppTargetUuid,
28
+ };
29
+ exceptionSection[`${uuid}_comment`] = comment;
30
+ exceptionRefs.push({ value: uuid, comment });
31
+ }
32
+ if (options.excludedFromWidget && options.excludedFromWidget.length > 0) {
33
+ const uuid = generateUuid(project);
34
+ const comment = `Exceptions for "${options.folderName}" folder in widget target`;
35
+ exceptionSection[uuid] = {
36
+ isa: 'PBXFileSystemSynchronizedBuildFileExceptionSet',
37
+ membershipExceptions: options.excludedFromWidget,
38
+ target: options.widgetTargetUuid,
39
+ };
40
+ exceptionSection[`${uuid}_comment`] = comment;
41
+ exceptionRefs.push({ value: uuid, comment });
42
+ }
43
+ // 1) SynchronizedRootGroup 객체 생성
44
+ const groupUuid = generateUuid(project);
45
+ const groupSection = (objects['PBXFileSystemSynchronizedRootGroup'] ??= {});
46
+ const groupComment = options.folderName;
47
+ const groupObject = {
48
+ isa: 'PBXFileSystemSynchronizedRootGroup',
49
+ path: options.folderName,
50
+ sourceTree: '"<group>"',
51
+ };
52
+ if (exceptionRefs.length > 0) {
53
+ groupObject.exceptions = exceptionRefs;
54
+ }
55
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
56
+ groupSection[groupUuid] = groupObject;
57
+ groupSection[`${groupUuid}_comment`] = groupComment;
58
+ // 2) 위젯 타겟의 fileSystemSynchronizedGroups에 등록
59
+ const targetSection = project.pbxNativeTargetSection();
60
+ const widgetTarget = targetSection[options.widgetTargetUuid];
61
+ if (!widgetTarget || typeof widgetTarget === 'string') {
62
+ throw new Error(`Widget target ${options.widgetTargetUuid} not found.`);
63
+ }
64
+ const synced = widgetTarget
65
+ .fileSystemSynchronizedGroups ?? [];
66
+ synced.push({ value: groupUuid, comment: groupComment });
67
+ widgetTarget.fileSystemSynchronizedGroups = synced;
68
+ }
69
+ function generateUuid(project) {
70
+ return project.generateUuid();
71
+ }
@@ -0,0 +1,25 @@
1
+ import type { PBXProject, PBXNativeTarget } from 'xcode';
2
+ import './types';
3
+ export interface AddTargetOptions {
4
+ /** 타겟 이름. 예: "ControlCenterExtension" → 빌드되면 ControlCenterExtension.appex */
5
+ name: string;
6
+ /** 위젯의 bundle id. 관례상 메인 앱 id 뒤에 부속 단어 붙임. */
7
+ bundleId: string;
8
+ }
9
+ export interface AddTargetResult {
10
+ uuid: string;
11
+ target: PBXNativeTarget;
12
+ }
13
+ /**
14
+ * 빈 사용자 Xcode 프로젝트에 Widget Extension 타겟을 1개 추가한다.
15
+ *
16
+ * 내부적으로 xcode 패키지의 addTarget()을 호출.
17
+ * targetType="app_extension"을 주면 패키지가 알아서 productType을
18
+ * "com.apple.product-type.app-extension"으로 매핑해줌.
19
+ *
20
+ * 주의: 이 함수는 타겟 노드만 만든다.
21
+ * - Frameworks 링크 → Day 3
22
+ * - Source 멤버십 → Day 4
23
+ * - 메인 앱에 임베드 → Day 5
24
+ */
25
+ export declare function addWidgetExtensionTarget(project: PBXProject, options: AddTargetOptions): AddTargetResult;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.addWidgetExtensionTarget = addWidgetExtensionTarget;
4
+ require("./types");
5
+ /**
6
+ * 빈 사용자 Xcode 프로젝트에 Widget Extension 타겟을 1개 추가한다.
7
+ *
8
+ * 내부적으로 xcode 패키지의 addTarget()을 호출.
9
+ * targetType="app_extension"을 주면 패키지가 알아서 productType을
10
+ * "com.apple.product-type.app-extension"으로 매핑해줌.
11
+ *
12
+ * 주의: 이 함수는 타겟 노드만 만든다.
13
+ * - Frameworks 링크 → Day 3
14
+ * - Source 멤버십 → Day 4
15
+ * - 메인 앱에 임베드 → Day 5
16
+ */
17
+ function addWidgetExtensionTarget(project, options) {
18
+ const { name, bundleId } = options;
19
+ const result = project.addTarget(name, // 타겟 이름
20
+ 'app_extension', // → "com.apple.product-type.app-extension" 자동 매핑
21
+ name, // subfolder (=name으로 통일)
22
+ bundleId // 위젯 bundle id
23
+ );
24
+ // xcode 패키지의 addTarget()은 새 타겟에 빈 buildPhases 배열만 만들어준다.
25
+ // Sources / Frameworks / Resources 페이즈는 우리가 직접 만들어 줘야
26
+ // 나중에 framework이나 source 파일을 등록할 곳이 생긴다.
27
+ project.addBuildPhase([], 'PBXSourcesBuildPhase', 'Sources', result.uuid);
28
+ project.addBuildPhase([], 'PBXFrameworksBuildPhase', 'Frameworks', result.uuid);
29
+ project.addBuildPhase([], 'PBXResourcesBuildPhase', 'Resources', result.uuid);
30
+ return {
31
+ uuid: result.uuid,
32
+ target: result.pbxNativeTarget,
33
+ };
34
+ }
@@ -0,0 +1,14 @@
1
+ import type { PBXProject } from 'xcode';
2
+ import './types';
3
+ /**
4
+ * 타겟의 모든 build configuration(Debug, Release 등)에 같은 설정값들을 일괄 적용한다.
5
+ *
6
+ * pbxproj 구조:
7
+ * PBXNativeTarget(타겟)
8
+ * └─ buildConfigurationList → XCConfigurationList(uuid)
9
+ * └─ buildConfigurations: [Debug uuid, Release uuid, ...]
10
+ * └─ 각 XCBuildConfiguration에 buildSettings dict
11
+ *
12
+ * 같은 키로 이미 값이 있으면 덮어씀.
13
+ */
14
+ export declare function setTargetBuildSettings(project: PBXProject, targetUuid: string, settings: Record<string, string>): void;