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,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setTargetBuildSettings = setTargetBuildSettings;
4
+ require("./types");
5
+ /**
6
+ * 타겟의 모든 build configuration(Debug, Release 등)에 같은 설정값들을 일괄 적용한다.
7
+ *
8
+ * pbxproj 구조:
9
+ * PBXNativeTarget(타겟)
10
+ * └─ buildConfigurationList → XCConfigurationList(uuid)
11
+ * └─ buildConfigurations: [Debug uuid, Release uuid, ...]
12
+ * └─ 각 XCBuildConfiguration에 buildSettings dict
13
+ *
14
+ * 같은 키로 이미 값이 있으면 덮어씀.
15
+ */
16
+ function setTargetBuildSettings(project, targetUuid, settings) {
17
+ const objects = project.hash.project.objects;
18
+ const targetSection = project.pbxNativeTargetSection();
19
+ const target = targetSection[targetUuid];
20
+ if (!target || typeof target === 'string') {
21
+ throw new Error(`Target ${targetUuid} not found.`);
22
+ }
23
+ const configListUuid = target.buildConfigurationList;
24
+ if (!configListUuid) {
25
+ throw new Error(`Target ${targetUuid} has no buildConfigurationList.`);
26
+ }
27
+ const configListSection = objects['XCConfigurationList'] ?? {};
28
+ const configList = configListSection[configListUuid];
29
+ if (!configList || typeof configList === 'string') {
30
+ throw new Error(`XCConfigurationList ${configListUuid} not found for target ${targetUuid}.`);
31
+ }
32
+ const buildConfigs = configList.buildConfigurations ?? [];
33
+ const buildConfigSection = objects['XCBuildConfiguration'] ?? {};
34
+ for (const ref of buildConfigs) {
35
+ const config = buildConfigSection[ref.value];
36
+ if (!config || typeof config === 'string')
37
+ continue;
38
+ const buildSettings = (config.buildSettings ??= {});
39
+ for (const [key, value] of Object.entries(settings)) {
40
+ buildSettings[key] = quoteIfNeeded(value);
41
+ }
42
+ }
43
+ }
44
+ /**
45
+ * pbxproj 형식상 공백/슬래시 등이 있으면 큰따옴표로 감싸야 한다.
46
+ * 단순 식별자는 그대로 둔다.
47
+ */
48
+ function quoteIfNeeded(value) {
49
+ if (/^[A-Za-z0-9_.-]+$/.test(value)) {
50
+ return value;
51
+ }
52
+ // 이미 따옴표로 감싸진 값은 다시 감싸지 않음
53
+ if (value.startsWith('"') && value.endsWith('"')) {
54
+ return value;
55
+ }
56
+ return `"${value}"`;
57
+ }
@@ -0,0 +1,16 @@
1
+ import type { PBXProject } from 'xcode';
2
+ import './types';
3
+ export interface EmbedCheckResult {
4
+ ok: boolean;
5
+ reason?: string;
6
+ }
7
+ /**
8
+ * 위젯이 메인 앱에 정상적으로 임베드되었는지 검증한다.
9
+ *
10
+ * xcode 패키지의 addTarget('app_extension', ...)이 호출되면 메인 앱 타겟에
11
+ * PBXCopyFilesBuildPhase가 자동으로 생성되고 .appex 파일이 거기 등록된다.
12
+ * 이 함수는 그 결과가 실제로 존재하는지 확인하는 안전망 역할.
13
+ *
14
+ * 임베드가 누락된 경우 사용할 수 있는 ensureEmbedded()도 함께 제공.
15
+ */
16
+ export declare function verifyEmbedded(project: PBXProject, mainAppTargetUuid: string, widgetTargetUuid: string): EmbedCheckResult;
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.verifyEmbedded = verifyEmbedded;
4
+ require("./types");
5
+ /**
6
+ * 위젯이 메인 앱에 정상적으로 임베드되었는지 검증한다.
7
+ *
8
+ * xcode 패키지의 addTarget('app_extension', ...)이 호출되면 메인 앱 타겟에
9
+ * PBXCopyFilesBuildPhase가 자동으로 생성되고 .appex 파일이 거기 등록된다.
10
+ * 이 함수는 그 결과가 실제로 존재하는지 확인하는 안전망 역할.
11
+ *
12
+ * 임베드가 누락된 경우 사용할 수 있는 ensureEmbedded()도 함께 제공.
13
+ */
14
+ function verifyEmbedded(project, mainAppTargetUuid, widgetTargetUuid) {
15
+ const objects = project.hash.project.objects;
16
+ // 1) 메인 앱 타겟에 PBXCopyFilesBuildPhase가 있는가?
17
+ const targets = project.pbxNativeTargetSection();
18
+ const mainTarget = targets[mainAppTargetUuid];
19
+ if (!mainTarget || typeof mainTarget === 'string') {
20
+ return { ok: false, reason: `Main app target ${mainAppTargetUuid} not found.` };
21
+ }
22
+ const copyFilesPhases = collectCopyFilesPhases(project, mainTarget);
23
+ if (copyFilesPhases.length === 0) {
24
+ return {
25
+ ok: false,
26
+ reason: 'Main app target has no PBXCopyFilesBuildPhase. Widget cannot be embedded.',
27
+ };
28
+ }
29
+ // 2) 위젯 타겟의 productReference 찾기 (.appex 의 PBXFileReference uuid)
30
+ const widgetTarget = targets[widgetTargetUuid];
31
+ if (!widgetTarget || typeof widgetTarget === 'string') {
32
+ return { ok: false, reason: `Widget target ${widgetTargetUuid} not found.` };
33
+ }
34
+ const productRef = widgetTarget.productReference;
35
+ if (!productRef) {
36
+ return { ok: false, reason: 'Widget target has no productReference.' };
37
+ }
38
+ // 3) 어느 CopyFiles 페이즈든 그 .appex를 가리키는 BuildFile을 가진 게 있는가?
39
+ const buildFileSection = project.pbxBuildFileSection();
40
+ for (const phase of copyFilesPhases) {
41
+ for (const ref of phase.files) {
42
+ const buildFile = buildFileSection[ref.value];
43
+ if (!buildFile || typeof buildFile === 'string')
44
+ continue;
45
+ if (buildFile.fileRef === productRef) {
46
+ return { ok: true };
47
+ }
48
+ }
49
+ }
50
+ return {
51
+ ok: false,
52
+ reason: 'No PBXCopyFilesBuildPhase entry references the widget productReference.',
53
+ };
54
+ }
55
+ function collectCopyFilesPhases(project, target) {
56
+ const objects = project.hash.project.objects;
57
+ const result = [];
58
+ for (const ref of target.buildPhases ?? []) {
59
+ const phase = findObjectAcrossSections(objects, ref.value);
60
+ if (phase && phase.isa === 'PBXCopyFilesBuildPhase') {
61
+ result.push(phase);
62
+ }
63
+ }
64
+ return result;
65
+ }
66
+ function findObjectAcrossSections(objects, uuid) {
67
+ for (const sectionName of Object.keys(objects)) {
68
+ const section = objects[sectionName];
69
+ if (section && uuid in section) {
70
+ return section[uuid];
71
+ }
72
+ }
73
+ return null;
74
+ }
@@ -0,0 +1,29 @@
1
+ import { type PBXProject } from 'xcode';
2
+ import './types';
3
+ /**
4
+ * 진단/디버그 전용 — 기존 pbxproj를 열어 구조를 요약.
5
+ * Day 1에서 실제 동작하는 Xcode 프로젝트가 어떻게 생겼는지 파악하는 데 사용.
6
+ */
7
+ export interface ProjectSummary {
8
+ filepath: string;
9
+ rootObjectUuid: string;
10
+ targets: TargetSummary[];
11
+ fileReferenceCount: number;
12
+ buildFileCount: number;
13
+ groupCount: number;
14
+ }
15
+ export interface TargetSummary {
16
+ uuid: string;
17
+ name: string;
18
+ productType: string;
19
+ productName: string;
20
+ buildPhases: BuildPhaseSummary[];
21
+ dependencyCount: number;
22
+ }
23
+ export interface BuildPhaseSummary {
24
+ uuid: string;
25
+ isa: string;
26
+ fileCount: number;
27
+ }
28
+ export declare function loadProject(filepath: string): PBXProject;
29
+ export declare function summarize(project: PBXProject): ProjectSummary;
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.loadProject = loadProject;
7
+ exports.summarize = summarize;
8
+ const xcode_1 = __importDefault(require("xcode"));
9
+ require("./types"); // ambient module declaration
10
+ function loadProject(filepath) {
11
+ const project = xcode_1.default.project(filepath);
12
+ project.parseSync();
13
+ return project;
14
+ }
15
+ function summarize(project) {
16
+ const root = project.getFirstProject();
17
+ const targets = enumerateTargets(project);
18
+ return {
19
+ filepath: project.filepath,
20
+ rootObjectUuid: root.uuid,
21
+ targets,
22
+ fileReferenceCount: countSection(project.pbxFileReferenceSection()),
23
+ buildFileCount: countSection(project.pbxBuildFileSection()),
24
+ groupCount: countSection(project.hash.project.objects['PBXGroup'] ?? {}),
25
+ };
26
+ }
27
+ function enumerateTargets(project) {
28
+ const section = project.pbxNativeTargetSection();
29
+ const summaries = [];
30
+ for (const [uuid, value] of Object.entries(section)) {
31
+ // _comment 키는 건너뜀
32
+ if (uuid.endsWith('_comment') || typeof value === 'string')
33
+ continue;
34
+ const target = value;
35
+ if (target.isa !== 'PBXNativeTarget')
36
+ continue;
37
+ summaries.push({
38
+ uuid,
39
+ name: stripQuotes(target.name),
40
+ productName: stripQuotes(target.productName),
41
+ productType: stripQuotes(target.productType),
42
+ dependencyCount: target.dependencies?.length ?? 0,
43
+ buildPhases: summarizeBuildPhases(project, target),
44
+ });
45
+ }
46
+ return summaries;
47
+ }
48
+ function summarizeBuildPhases(project, target) {
49
+ const summaries = [];
50
+ const objects = project.hash.project.objects;
51
+ for (const ref of target.buildPhases) {
52
+ const sectionName = (ref.comment ?? '').replace(/^.*:\s*/, '');
53
+ const phase = findObjectAcrossSections(objects, ref.value);
54
+ if (!phase)
55
+ continue;
56
+ summaries.push({
57
+ uuid: ref.value,
58
+ isa: phase.isa ?? sectionName,
59
+ fileCount: (phase.files ?? []).length,
60
+ });
61
+ }
62
+ return summaries;
63
+ }
64
+ function findObjectAcrossSections(objects, uuid) {
65
+ for (const sectionName of Object.keys(objects)) {
66
+ const section = objects[sectionName];
67
+ if (section && uuid in section) {
68
+ return section[uuid];
69
+ }
70
+ }
71
+ return null;
72
+ }
73
+ function countSection(section) {
74
+ return Object.keys(section).filter((k) => !k.endsWith('_comment')).length;
75
+ }
76
+ /**
77
+ * pbxproj는 공백이나 특수문자가 있으면 문자열을 큰따옴표로 감싸서 저장한다.
78
+ * 단순 이름 비교를 위해 둘러싼 따옴표만 제거한다.
79
+ */
80
+ function stripQuotes(value) {
81
+ if (!value)
82
+ return '';
83
+ if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
84
+ return value.slice(1, -1);
85
+ }
86
+ return value;
87
+ }
@@ -0,0 +1,18 @@
1
+ import type { PBXProject } from 'xcode';
2
+ import './types';
3
+ export interface LinkFrameworksOptions {
4
+ /** 기본 SDK framework 이름들. 예: ['WidgetKit', 'SwiftUI', 'AppIntents'] */
5
+ frameworks: string[];
6
+ }
7
+ /**
8
+ * 지정된 타겟에 iOS SDK framework들을 링크한다.
9
+ *
10
+ * 핵심 매커니즘 (Day 1에 배운 그대로):
11
+ * - 같은 framework가 이미 프로젝트에 있으면 → PBXFileReference 재사용,
12
+ * PBXBuildFile만 새로 만들어서 이 타겟의 Frameworks 빌드 페이즈에 추가
13
+ * - 처음 추가하는 framework면 → xcode 패키지의 addFramework() 호출 (모든 작업
14
+ * 알아서 해줌: FileRef + BuildFile + Frameworks phase 등록)
15
+ *
16
+ * 다른 타겟에도 같은 framework 링크하려면 다른 targetUuid로 다시 호출.
17
+ */
18
+ export declare function linkFrameworks(project: PBXProject, targetUuid: string, options: LinkFrameworksOptions): void;
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.linkFrameworks = linkFrameworks;
4
+ require("./types");
5
+ /**
6
+ * 지정된 타겟에 iOS SDK framework들을 링크한다.
7
+ *
8
+ * 핵심 매커니즘 (Day 1에 배운 그대로):
9
+ * - 같은 framework가 이미 프로젝트에 있으면 → PBXFileReference 재사용,
10
+ * PBXBuildFile만 새로 만들어서 이 타겟의 Frameworks 빌드 페이즈에 추가
11
+ * - 처음 추가하는 framework면 → xcode 패키지의 addFramework() 호출 (모든 작업
12
+ * 알아서 해줌: FileRef + BuildFile + Frameworks phase 등록)
13
+ *
14
+ * 다른 타겟에도 같은 framework 링크하려면 다른 targetUuid로 다시 호출.
15
+ */
16
+ function linkFrameworks(project, targetUuid, options) {
17
+ for (const name of options.frameworks) {
18
+ const path = `System/Library/Frameworks/${name}.framework`;
19
+ const existingFileRefUuid = findFileReferenceUuid(project, path);
20
+ if (existingFileRefUuid) {
21
+ // 두 번째 이상의 타겟 — 새 BuildFile만 만들어서 기존 FileRef를 가리키게 함
22
+ addAdditionalBuildFile(project, targetUuid, existingFileRefUuid, `${name}.framework`);
23
+ }
24
+ else {
25
+ // 처음 추가 — xcode 패키지가 FileRef + BuildFile + Frameworks 모두 처리
26
+ project.addFramework(path, { target: targetUuid });
27
+ }
28
+ }
29
+ }
30
+ /**
31
+ * PBXFileReference 섹션을 훑어 같은 path 가진 항목 찾기.
32
+ * pbxproj 형식상 path는 "..." 로 감싸진 형태일 수도 있어 둘 다 매칭.
33
+ */
34
+ function findFileReferenceUuid(project, path) {
35
+ const section = project.pbxFileReferenceSection();
36
+ for (const [uuid, value] of Object.entries(section)) {
37
+ if (uuid.endsWith('_comment') || typeof value === 'string')
38
+ continue;
39
+ const ref = value;
40
+ const refPath = ref.path ?? '';
41
+ if (refPath === path || refPath === `"${path}"`) {
42
+ return uuid;
43
+ }
44
+ }
45
+ return null;
46
+ }
47
+ /**
48
+ * 기존 FileReference를 가리키는 새 PBXBuildFile을 만들고
49
+ * 지정 타겟의 PBXFrameworksBuildPhase에 등록.
50
+ */
51
+ function addAdditionalBuildFile(project, targetUuid, fileRefUuid, basename) {
52
+ const objects = project.hash.project.objects;
53
+ // 1) 새 BuildFile 객체 생성
54
+ const buildFileSection = (objects['PBXBuildFile'] ??= {});
55
+ const buildFileUuid = generateUuid(project);
56
+ const comment = `${basename} in Frameworks`;
57
+ const buildFile = {
58
+ isa: 'PBXBuildFile',
59
+ fileRef: fileRefUuid,
60
+ // xcode 패키지가 빈 객체 자리에 fileRef_comment 같은 걸 넣어도 OK
61
+ };
62
+ buildFileSection[buildFileUuid] = buildFile;
63
+ buildFileSection[`${buildFileUuid}_comment`] = comment;
64
+ // 2) 타겟의 Frameworks 빌드 페이즈에 추가
65
+ const frameworksPhase = project.pbxFrameworksBuildPhaseObj(targetUuid);
66
+ if (!frameworksPhase) {
67
+ throw new Error(`Target ${targetUuid} has no PBXFrameworksBuildPhase`);
68
+ }
69
+ frameworksPhase.files.push({
70
+ value: buildFileUuid,
71
+ comment,
72
+ });
73
+ }
74
+ /**
75
+ * xcode 패키지의 generateUuid를 노출해 사용.
76
+ */
77
+ function generateUuid(project) {
78
+ // 패키지가 prototype에 generateUuid를 갖고 있음 — 타입 선언엔 빠져있어 캐스트
79
+ return project.generateUuid();
80
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * xcode npm 패키지(@3.0.1)는 자체 타입 선언을 제공하지 않으므로
3
+ * 우리가 사용하는 표면만 최소한으로 모델링.
4
+ *
5
+ * 패키지의 실제 모양은 lib/pbxProject.js (1700+ 줄) 참조.
6
+ */
7
+ declare module 'xcode' {
8
+ interface PBXObject {
9
+ isa: string;
10
+ [key: string]: unknown;
11
+ }
12
+ interface PBXNativeTarget extends PBXObject {
13
+ isa: 'PBXNativeTarget';
14
+ name: string;
15
+ productName: string;
16
+ productType: string;
17
+ productReference?: string;
18
+ buildPhases: Array<{
19
+ value: string;
20
+ comment?: string;
21
+ }>;
22
+ buildConfigurationList: string;
23
+ dependencies: Array<{
24
+ value: string;
25
+ comment?: string;
26
+ }>;
27
+ }
28
+ interface PBXFileReference extends PBXObject {
29
+ isa: 'PBXFileReference';
30
+ name?: string;
31
+ path?: string;
32
+ sourceTree: string;
33
+ lastKnownFileType?: string;
34
+ explicitFileType?: string;
35
+ fileEncoding?: number;
36
+ includeInIndex?: number | string;
37
+ }
38
+ interface PBXBuildFile extends PBXObject {
39
+ isa: 'PBXBuildFile';
40
+ fileRef: string;
41
+ settings?: {
42
+ ATTRIBUTES?: string[];
43
+ };
44
+ }
45
+ interface PBXGroup extends PBXObject {
46
+ isa: 'PBXGroup';
47
+ children: Array<{
48
+ value: string;
49
+ comment?: string;
50
+ }>;
51
+ name?: string;
52
+ path?: string;
53
+ sourceTree: string;
54
+ }
55
+ interface PBXBuildPhase extends PBXObject {
56
+ isa: 'PBXFrameworksBuildPhase' | 'PBXSourcesBuildPhase' | 'PBXResourcesBuildPhase' | 'PBXCopyFilesBuildPhase' | 'PBXShellScriptBuildPhase';
57
+ files: Array<{
58
+ value: string;
59
+ comment?: string;
60
+ }>;
61
+ buildActionMask: number;
62
+ runOnlyForDeploymentPostprocessing: number;
63
+ name?: string;
64
+ dstPath?: string;
65
+ dstSubfolderSpec?: number;
66
+ }
67
+ interface PBXProjectInternals {
68
+ objects: Record<string, Record<string, PBXObject | string>>;
69
+ rootObject: string;
70
+ }
71
+ interface PBXProjectHash {
72
+ project: PBXProjectInternals;
73
+ headComment?: string;
74
+ }
75
+ interface PBXProject {
76
+ filepath: string;
77
+ hash: PBXProjectHash;
78
+ parse(callback: (err: Error | null) => void): void;
79
+ parseSync(): void;
80
+ writeSync(): string;
81
+ getFirstProject(): {
82
+ uuid: string;
83
+ firstProject: PBXObject;
84
+ };
85
+ getFirstTarget(): {
86
+ uuid: string;
87
+ firstTarget: PBXNativeTarget;
88
+ };
89
+ getTarget(productType: string): {
90
+ uuid: string;
91
+ target: PBXNativeTarget;
92
+ } | null;
93
+ pbxNativeTargetSection(): Record<string, PBXNativeTarget | string>;
94
+ pbxFileReferenceSection(): Record<string, PBXFileReference | string>;
95
+ pbxBuildFileSection(): Record<string, PBXBuildFile | string>;
96
+ pbxFrameworksBuildPhaseObj(targetUuid: string): PBXBuildPhase;
97
+ pbxSourcesBuildPhaseObj(targetUuid: string): PBXBuildPhase;
98
+ pbxCopyfilesBuildPhaseObj(targetUuid: string): PBXBuildPhase | undefined;
99
+ pbxGroupByName(name: string): PBXGroup | undefined;
100
+ pbxTargetByName(name: string): PBXNativeTarget | undefined;
101
+ addTarget(name: string, type: string, subfolder?: string, bundleId?: string): {
102
+ uuid: string;
103
+ pbxNativeTarget: PBXNativeTarget;
104
+ };
105
+ addBuildPhase(filePathsArray: string[], buildPhaseType: string, comment: string, target: string, optionsOrFolderType?: object | string, subfolderPath?: string): {
106
+ uuid: string;
107
+ buildPhase: PBXBuildPhase;
108
+ };
109
+ addFramework(filepath: string, opt?: {
110
+ target?: string;
111
+ weak?: boolean;
112
+ }): unknown;
113
+ addToPbxBuildFileSection(file: unknown): void;
114
+ addToPbxFileReferenceSection(file: unknown): void;
115
+ addPbxGroup(filePathsArray: string[], name: string, path: string, sourceTree?: string): {
116
+ uuid: string;
117
+ pbxGroup: PBXGroup;
118
+ };
119
+ }
120
+ function project(filepath: string): PBXProject;
121
+ }
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ /**
3
+ * xcode npm 패키지(@3.0.1)는 자체 타입 선언을 제공하지 않으므로
4
+ * 우리가 사용하는 표면만 최소한으로 모델링.
5
+ *
6
+ * 패키지의 실제 모양은 lib/pbxProject.js (1700+ 줄) 참조.
7
+ */
@@ -0,0 +1,27 @@
1
+ import type { PBXProject } from 'xcode';
2
+ import './types';
3
+ export interface WireOptions {
4
+ /** 메인 앱 bundle id. 예: "com.acme.app" */
5
+ mainAppBundleId: string;
6
+ /** 위젯 타겟 이름 + 폴더 이름. 예: "ControlCenterExtension" */
7
+ widgetTargetName: string;
8
+ /** 위젯 bundle id. 관례상 메인 앱 id의 하위. 예: "com.acme.app.controlcenter" */
9
+ widgetBundleId: string;
10
+ /** 메인 앱과 공유할 폴더 내 파일 (폴더 기준 상대 경로) */
11
+ sharedFiles: string[];
12
+ /** 위젯 deployment target. 기본 18.0 */
13
+ deploymentTarget?: string;
14
+ /** Swift 버전. 기본 5.0 */
15
+ swiftVersion?: string;
16
+ }
17
+ export interface WireResult {
18
+ widgetTargetUuid: string;
19
+ mainAppTargetUuid: string;
20
+ }
21
+ /**
22
+ * Day 2~6의 모든 부품을 순서대로 호출해 사용자 Xcode 프로젝트에 위젯 익스텐션을
23
+ * 완전히 통합한다.
24
+ *
25
+ * 호출자(plugin/ 또는 cli/)는 이 함수 하나만 부르면 됨.
26
+ */
27
+ export declare function wireXcodeProject(project: PBXProject, options: WireOptions): WireResult;
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.wireXcodeProject = wireXcodeProject;
4
+ require("./types");
5
+ const addTarget_1 = require("./addTarget");
6
+ const linkFrameworks_1 = require("./linkFrameworks");
7
+ const addSyncedFolder_1 = require("./addSyncedFolder");
8
+ const buildSettings_1 = require("./buildSettings");
9
+ const embed_1 = require("./embed");
10
+ /**
11
+ * Day 2~6의 모든 부품을 순서대로 호출해 사용자 Xcode 프로젝트에 위젯 익스텐션을
12
+ * 완전히 통합한다.
13
+ *
14
+ * 호출자(plugin/ 또는 cli/)는 이 함수 하나만 부르면 됨.
15
+ */
16
+ function wireXcodeProject(project, options) {
17
+ const deploymentTarget = options.deploymentTarget ?? '18.0';
18
+ const swiftVersion = options.swiftVersion ?? '5.0';
19
+ // 0) 메인 앱 타겟 찾기
20
+ const mainAppTargetUuid = findMainAppTargetUuid(project);
21
+ if (!mainAppTargetUuid) {
22
+ throw new Error('Could not find main application target in the Xcode project.');
23
+ }
24
+ // 1) 위젯 타겟 생성 (자동으로 메인 앱에 CopyFiles 임베드까지 됨)
25
+ const { uuid: widgetTargetUuid } = (0, addTarget_1.addWidgetExtensionTarget)(project, {
26
+ name: options.widgetTargetName,
27
+ bundleId: options.widgetBundleId,
28
+ });
29
+ // 2) Frameworks 링크
30
+ (0, linkFrameworks_1.linkFrameworks)(project, widgetTargetUuid, {
31
+ frameworks: ['WidgetKit', 'SwiftUI', 'AppIntents'],
32
+ });
33
+ (0, linkFrameworks_1.linkFrameworks)(project, mainAppTargetUuid, {
34
+ frameworks: ['AppIntents'],
35
+ });
36
+ // 3) Synced 폴더 + ExceptionSet
37
+ // - sharedFiles는 메인 앱이 추가로 가져가야 하므로 mainAppTargetUuid 쪽 예외
38
+ // - Info.plist와 entitlements는 위젯 build settings로 참조되므로 위젯의 자동
39
+ // 멤버십에서는 빼야 "Multiple commands produce" 충돌이 안 남
40
+ const widgetExclusions = [
41
+ 'Info.plist',
42
+ `${options.widgetTargetName}.entitlements`,
43
+ 'MainApp.entitlements',
44
+ ];
45
+ (0, addSyncedFolder_1.addSyncedSourceFolder)(project, {
46
+ widgetTargetUuid,
47
+ mainAppTargetUuid,
48
+ folderName: options.widgetTargetName,
49
+ sharedFiles: options.sharedFiles,
50
+ excludedFromWidget: widgetExclusions,
51
+ });
52
+ // 4) 위젯 타겟 build settings
53
+ const widgetEntitlementsPath = `${options.widgetTargetName}/${options.widgetTargetName}.entitlements`;
54
+ const mainEntitlementsPath = `${options.widgetTargetName}/MainApp.entitlements`;
55
+ (0, buildSettings_1.setTargetBuildSettings)(project, widgetTargetUuid, {
56
+ IPHONEOS_DEPLOYMENT_TARGET: deploymentTarget,
57
+ INFOPLIST_FILE: `${options.widgetTargetName}/Info.plist`,
58
+ // 우리가 직접 Info.plist를 만들기 때문에 Xcode 자동 생성을 끔.
59
+ // 안 끄면 "Multiple commands produce Info.plist" 빌드 충돌 발생.
60
+ GENERATE_INFOPLIST_FILE: 'NO',
61
+ CODE_SIGN_ENTITLEMENTS: widgetEntitlementsPath,
62
+ SWIFT_VERSION: swiftVersion,
63
+ PRODUCT_BUNDLE_IDENTIFIER: options.widgetBundleId,
64
+ SKIP_INSTALL: 'NO',
65
+ });
66
+ // 5) 메인 앱 타겟 build settings.
67
+ // - entitlement 경로 (App Group 공유)
68
+ // - 공유되는 Intent 파일이 LocalizedStringResource 등 iOS 16+ API를 쓰므로
69
+ // 배포 타겟이 16.0보다 낮으면 16.0으로 올림 (이미 16+면 사용자 값 유지).
70
+ const mainAppSettings = {
71
+ CODE_SIGN_ENTITLEMENTS: mainEntitlementsPath,
72
+ };
73
+ const currentMainTarget = readDeploymentTarget(project, mainAppTargetUuid);
74
+ if (currentMainTarget === null || compareVersion(currentMainTarget, '16.0') < 0) {
75
+ mainAppSettings.IPHONEOS_DEPLOYMENT_TARGET = '16.0';
76
+ }
77
+ (0, buildSettings_1.setTargetBuildSettings)(project, mainAppTargetUuid, mainAppSettings);
78
+ // 6) 임베드 검증
79
+ const embedCheck = (0, embed_1.verifyEmbedded)(project, mainAppTargetUuid, widgetTargetUuid);
80
+ if (!embedCheck.ok) {
81
+ throw new Error(`Widget embedding verification failed: ${embedCheck.reason}`);
82
+ }
83
+ return { widgetTargetUuid, mainAppTargetUuid };
84
+ }
85
+ function findMainAppTargetUuid(project) {
86
+ const section = project.pbxNativeTargetSection();
87
+ for (const [uuid, value] of Object.entries(section)) {
88
+ if (uuid.endsWith('_comment') || typeof value === 'string')
89
+ continue;
90
+ const target = value;
91
+ if (target.isa !== 'PBXNativeTarget')
92
+ continue;
93
+ const productType = stripQuotes(target.productType);
94
+ if (productType === 'com.apple.product-type.application') {
95
+ return uuid;
96
+ }
97
+ }
98
+ return null;
99
+ }
100
+ function stripQuotes(value) {
101
+ if (!value)
102
+ return '';
103
+ if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
104
+ return value.slice(1, -1);
105
+ }
106
+ return value;
107
+ }
108
+ function readDeploymentTarget(project, targetUuid) {
109
+ const objects = project.hash.project.objects;
110
+ const target = project.pbxNativeTargetSection()[targetUuid];
111
+ if (!target)
112
+ return null;
113
+ const configListUuid = target.buildConfigurationList;
114
+ if (!configListUuid)
115
+ return null;
116
+ const configList = objects['XCConfigurationList']?.[configListUuid];
117
+ if (!configList)
118
+ return null;
119
+ const buildConfigs = configList.buildConfigurations ?? [];
120
+ for (const ref of buildConfigs) {
121
+ const config = objects['XCBuildConfiguration']?.[ref.value];
122
+ if (!config)
123
+ continue;
124
+ const settings = config.buildSettings;
125
+ const v = settings?.IPHONEOS_DEPLOYMENT_TARGET;
126
+ if (v)
127
+ return stripQuotes(v);
128
+ }
129
+ return null;
130
+ }
131
+ function compareVersion(a, b) {
132
+ const pa = a.split('.').map((n) => parseInt(n, 10));
133
+ const pb = b.split('.').map((n) => parseInt(n, 10));
134
+ const len = Math.max(pa.length, pb.length);
135
+ for (let i = 0; i < len; i++) {
136
+ const ai = pa[i] ?? 0;
137
+ const bi = pb[i] ?? 0;
138
+ if (ai !== bi)
139
+ return ai - bi;
140
+ }
141
+ return 0;
142
+ }