@tantawowa/hosanna-tools 3.20.0 → 3.22.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.
- package/README.md +3 -1
- package/dist/build-info.json +3 -3
- package/dist/native/native-project.js +318 -3
- package/dist/native/native-project.js.map +1 -1
- package/dist/native/templates/apple/HosannaTvApp.swift +63 -8
- package/dist/native/templates/apple/project.yml +1 -4
- package/dist/run/android-emulator.d.ts +1 -0
- package/dist/run/android-emulator.js +7 -1
- package/dist/run/android-emulator.js.map +1 -1
- package/dist/testing/client.js +39 -14
- package/dist/testing/client.js.map +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +2 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/launcher.d.ts +2 -1
- package/dist/testing/launcher.js +8 -3
- package/dist/testing/launcher.js.map +1 -1
- package/dist/testing/native-simulator.d.ts +2 -2
- package/dist/testing/native-simulator.js +23 -3
- package/dist/testing/native-simulator.js.map +1 -1
- package/dist/testing/types.d.ts +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -350,6 +350,8 @@ Bare `hst run` in an interactive terminal discovers runnable web previews, vendo
|
|
|
350
350
|
|
|
351
351
|
`browser` is the canonical public platform; legacy platform input `web` is normalized to `browser`. `emulator` is the public desktop-preview target and HST retains internal target kind `web`. For Tizen and webOS, emulator mode launches the target-specific Vite config so compile-time initializers are selected; it is not a vendor simulator. `--target sim` launches a real vendor virtual device only for supported Apple/Android platforms. `--target device` launches or deploys to physical local hardware. Device/sim runs record the target and refuse to overwrite a recorded session or safe-to-kill occupied resource in non-interactive mode unless `--replace` is passed. Tizen/webOS previews cannot reuse a Browser session because their compiled alias graphs differ. Preferred run defaults live in `.hosanna-tools/run.json`, and friendly device aliases live in `.hosanna-tools/devices.json` or `~/.hosanna-tools/devices.json`; signing keys and device passphrases remain in vendor stores.
|
|
352
352
|
|
|
353
|
+
HST starts new Android virtual devices with the Android Emulator's recommended `auto` graphics mode. Set `HS_ANDROID_EMULATOR_GPU` to an emulator-supported mode such as `host` or `software` when a machine needs an explicit performance or compatibility override. HST reads this setting only when it starts the AVD; stop and restart an already-running emulator through HST after changing it. The value is passed directly to the installed emulator, so its available modes remain authoritative.
|
|
354
|
+
|
|
353
355
|
### Roku Commands (`roku:*`) - Roku Deployment & Packaging
|
|
354
356
|
- `roku:run` - Deploy a Roku app to a device (supports .zip file or folder)
|
|
355
357
|
- `roku:command` - Send a Hosanna command to an installed channel through Roku ECP
|
|
@@ -666,7 +668,7 @@ Screenshots and proofs wait for the command-plane hierarchy and focus to remain
|
|
|
666
668
|
|
|
667
669
|
Custom test ports must be unique. Keep the Vite and browser CDP ports outside the debugger's dynamic app allocation range; the launcher validates this before starting any processes and reports the conflicting range.
|
|
668
670
|
|
|
669
|
-
Keep structural and state assertions as the default correctness checks. Use `hs.screenshot.match(...)` only for an explicit visual regression whose target-specific baseline and tolerance are intentionally maintained. Failure diagnostics and proof artifacts are separated below `test-results/<testRunId>/<platform>-<target>/`.
|
|
671
|
+
Keep structural and state assertions as the default correctness checks. Use `hs.screenshot.match(...)` only for an explicit visual regression whose target-specific baseline and tolerance are intentionally maintained. Set `HS_UPDATE_SCREENSHOTS=1` only after reviewing a candidate: update mode creates missing baselines, replaces existing baselines even when the old comparison differs, and treats the captured frame as the new passing expectation. Failure diagnostics and proof artifacts are separated below `test-results/<testRunId>/<platform>-<target>/`.
|
|
670
672
|
|
|
671
673
|
`buildConfig` supplies a non-secret overlay that is merged last for the owned test launch. It may be a target-aware factory, so browser emulation can enable deterministic mocks while a physical Roku keeps its real platform services. The launcher and resolver both reject test overlays for `prod`:
|
|
672
674
|
|
package/dist/build-info.json
CHANGED
|
@@ -12,10 +12,11 @@ exports.formatNativeDoctorReportText = formatNativeDoctorReportText;
|
|
|
12
12
|
const crypto_1 = __importDefault(require("crypto"));
|
|
13
13
|
const fs_1 = __importDefault(require("fs"));
|
|
14
14
|
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const minimatch_1 = require("minimatch");
|
|
15
16
|
const DEFAULT_REFERENCE_ROOT = 'hosanna-ui';
|
|
16
17
|
const RUN_CONFIG_PATH = '.hosanna-tools/run.json';
|
|
17
18
|
const METADATA_PATH = '.hosanna-tools/native-project.json';
|
|
18
|
-
const TEMPLATE_VERSION = 'app-owned-native-
|
|
19
|
+
const TEMPLATE_VERSION = 'app-owned-native-v10-tvos-debug-overlay-input-capture';
|
|
19
20
|
const ANIMATED_IMAGE_TEMPLATE_VERSION = 8;
|
|
20
21
|
const ANIMATED_IMAGE_MIGRATION_PATHS = [
|
|
21
22
|
'platforms/apple/project.yml',
|
|
@@ -28,6 +29,16 @@ const PROJECT_MANAGED_GRADLE_WRAPPER_MIGRATION_PATHS = [
|
|
|
28
29
|
'platforms/android/gradle/wrapper/gradle-wrapper.jar',
|
|
29
30
|
'platforms/android/gradle/wrapper/gradle-wrapper.properties',
|
|
30
31
|
];
|
|
32
|
+
const TVOS_DEBUG_OVERLAY_CAPTURE_TEMPLATE_VERSION = 10;
|
|
33
|
+
const APPLE_PROJECT_PATH = 'platforms/apple/project.yml';
|
|
34
|
+
const TVOS_ROOT_HOST_PATH = 'platforms/apple/hosanna-apple/HosannaTvApp.swift';
|
|
35
|
+
const TVOS_LEGACY_HOST_PATH = 'platforms/apple/hosanna-apple/src/app/RootViewController.swift';
|
|
36
|
+
const TVOS_ROOT_HOST_SOURCE_PATH = 'hosanna-apple/HosannaTvApp.swift';
|
|
37
|
+
const TVOS_LEGACY_ENTRY_POINT_PATH = 'platforms/apple/hosanna-apple/src/app/AppDelegate.swift';
|
|
38
|
+
const TVOS_LEGACY_ENTRY_POINT_SOURCE_PATH = 'hosanna-apple/src/app/AppDelegate.swift';
|
|
39
|
+
const TVOS_DEBUG_OVERLAY_CAPTURE_MIGRATION_PATHS = [
|
|
40
|
+
TVOS_ROOT_HOST_PATH,
|
|
41
|
+
];
|
|
31
42
|
const NATIVE_PLATFORM_ORDER = [
|
|
32
43
|
'ios',
|
|
33
44
|
'apple-tv',
|
|
@@ -93,6 +104,14 @@ function createNativeInitPlan(input) {
|
|
|
93
104
|
const isConfiguringAndroidAutoMedia = identity.androidAutoMediaBrowserService !== undefined;
|
|
94
105
|
const animatedImageMigrationPaths = animatedImageManagedMigrationPaths(existingMetadata);
|
|
95
106
|
const projectManagedGradleWrapperMigrationPaths = managedMigrationPaths(existingMetadata, PROJECT_MANAGED_GRADLE_WRAPPER_MIGRATION_PATHS, PROJECT_MANAGED_GRADLE_WRAPPER_TEMPLATE_VERSION);
|
|
107
|
+
const currentAppleTvHostSelection = readAppleTvHostSelection(cwd);
|
|
108
|
+
const willReplaceAppleProject = Boolean(input.replace)
|
|
109
|
+
|| isAddingPlatform
|
|
110
|
+
|| animatedImageMigrationPaths.includes(APPLE_PROJECT_PATH);
|
|
111
|
+
const shouldMigrateRootTvHost = willReplaceAppleProject
|
|
112
|
+
|| !currentAppleTvHostSelection.legacyConfigured
|
|
113
|
+
|| currentAppleTvHostSelection.rootConfigured;
|
|
114
|
+
const tvosDebugOverlayCaptureMigrationPaths = managedMigrationPaths(existingMetadata, shouldMigrateRootTvHost ? TVOS_DEBUG_OVERLAY_CAPTURE_MIGRATION_PATHS : [], TVOS_DEBUG_OVERLAY_CAPTURE_TEMPLATE_VERSION);
|
|
96
115
|
const legacySymlinks = createLegacySymlinkPlan(cwd, referenceRoot, requestedPlatforms, errors);
|
|
97
116
|
validatePlatformRoots(cwd, requestedPlatforms, existingMetadata, errors);
|
|
98
117
|
const definitions = createTemplateDefinitions(cwd, referenceRoot, referenceRootInput, managedPlatforms, identity);
|
|
@@ -109,6 +128,7 @@ function createNativeInitPlan(input) {
|
|
|
109
128
|
: []),
|
|
110
129
|
...animatedImageMigrationPaths,
|
|
111
130
|
...projectManagedGradleWrapperMigrationPaths,
|
|
131
|
+
...tvosDebugOverlayCaptureMigrationPaths,
|
|
112
132
|
],
|
|
113
133
|
referenceRootInput,
|
|
114
134
|
platforms: managedPlatforms,
|
|
@@ -117,6 +137,7 @@ function createNativeInitPlan(input) {
|
|
|
117
137
|
appendAndroidAutoMediaPreservationWarnings(projectFiles, warnings);
|
|
118
138
|
}
|
|
119
139
|
appendAnimatedImageDependencyWarnings(projectFiles, warnings);
|
|
140
|
+
appendTvOsDebugOverlayCaptureWarnings(cwd, projectFiles, requestedPlatforms, warnings);
|
|
120
141
|
const directories = createDirectoryPlan(cwd, projectFiles);
|
|
121
142
|
const editableFiles = projectFiles.filter(item => isEditableProjectFile(item.path));
|
|
122
143
|
const gitignore = createGitignorePlan(cwd);
|
|
@@ -212,6 +233,7 @@ function createNativeDoctorReport(input) {
|
|
|
212
233
|
const missing = [];
|
|
213
234
|
const errors = [...plan.errors];
|
|
214
235
|
const animatedImageIssues = animatedImageDependencyIssues(plan.projectFiles);
|
|
236
|
+
const tvosDebugOverlayCaptureIssues = tvOsDebugOverlayCaptureIssues(plan.cwd, plan.projectFiles, plan.requestedPlatforms);
|
|
215
237
|
if (!fs_1.default.existsSync(plan.referenceRoot)) {
|
|
216
238
|
errors.push(`Reference root ${relative(plan.cwd, plan.referenceRoot)} does not exist. Run hst sdk:install before building native targets.`);
|
|
217
239
|
}
|
|
@@ -225,12 +247,16 @@ function createNativeDoctorReport(input) {
|
|
|
225
247
|
errors.push(`${item.path} is a legacy whole-directory symlink. Run ${fixCommand(input.platform)} to migrate it to an app-owned thin project.`);
|
|
226
248
|
}
|
|
227
249
|
}
|
|
250
|
+
const hasReadableMetadata = readNativeProjectMetadata(plan.cwd) !== undefined;
|
|
228
251
|
for (const item of plan.projectFiles) {
|
|
229
252
|
if (item.action !== 'preserve') {
|
|
253
|
+
if (item.path === METADATA_PATH && item.action === 'replace' && hasReadableMetadata) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
230
256
|
missing.push(item.path);
|
|
231
257
|
}
|
|
232
258
|
}
|
|
233
|
-
errors.push(...animatedImageIssues);
|
|
259
|
+
errors.push(...animatedImageIssues, ...tvosDebugOverlayCaptureIssues);
|
|
234
260
|
return {
|
|
235
261
|
schemaVersion: 1,
|
|
236
262
|
cwd: plan.cwd,
|
|
@@ -239,7 +265,7 @@ function createNativeDoctorReport(input) {
|
|
|
239
265
|
ok: missing.length === 0 && errors.length === 0,
|
|
240
266
|
fixCommand: fixCommand(input.platform),
|
|
241
267
|
missing,
|
|
242
|
-
warnings: plan.warnings.filter(warning => !animatedImageIssues.includes(warning)),
|
|
268
|
+
warnings: plan.warnings.filter(warning => (!animatedImageIssues.includes(warning) && !tvosDebugOverlayCaptureIssues.includes(warning))),
|
|
243
269
|
errors,
|
|
244
270
|
};
|
|
245
271
|
}
|
|
@@ -643,6 +669,295 @@ function countOccurrences(content, fragment) {
|
|
|
643
669
|
return 0;
|
|
644
670
|
return content.split(fragment).length - 1;
|
|
645
671
|
}
|
|
672
|
+
function appendTvOsDebugOverlayCaptureWarnings(cwd, projectFiles, platforms, warnings) {
|
|
673
|
+
warnings.push(...tvOsDebugOverlayCaptureIssues(cwd, projectFiles, platforms));
|
|
674
|
+
}
|
|
675
|
+
function tvOsDebugOverlayCaptureIssues(cwd, projectFiles, platforms) {
|
|
676
|
+
if (!platforms.includes('apple-tv'))
|
|
677
|
+
return [];
|
|
678
|
+
const appleProjectItem = projectFiles.find(file => file.path === APPLE_PROJECT_PATH);
|
|
679
|
+
if (!appleProjectItem || !fs_1.default.existsSync(appleProjectItem.absolutePath))
|
|
680
|
+
return [];
|
|
681
|
+
const selection = readAppleTvHostSelection(cwd);
|
|
682
|
+
if (selection.rootConfigured && selection.legacyConfigured) {
|
|
683
|
+
return [
|
|
684
|
+
`Apple TV target hosanna-apple in ${APPLE_PROJECT_PATH} compiles both ${TVOS_ROOT_HOST_SOURCE_PATH} and legacy hosanna-apple/src/app, creating multiple @main entry points. Keep exactly one host: retain \`- path: hosanna-apple/src/app\` and remove or exclude ${TVOS_ROOT_HOST_SOURCE_PATH} for the legacy UIKit host, or retain \`- path: ${TVOS_ROOT_HOST_SOURCE_PATH}\` and remove or exclude legacy src/app for the SwiftUI host; then rerun \`hst native:doctor apple\`.`,
|
|
685
|
+
];
|
|
686
|
+
}
|
|
687
|
+
if (!selection.rootConfigured && !selection.legacyConfigured) {
|
|
688
|
+
return [
|
|
689
|
+
`Apple TV target hosanna-apple in ${APPLE_PROJECT_PATH} does not compile a supported app host. Configure exactly one of \`- path: ${TVOS_ROOT_HOST_SOURCE_PATH}\` or \`- path: hosanna-apple/src/app\`, then rerun \`hst native:doctor apple\`.`,
|
|
690
|
+
];
|
|
691
|
+
}
|
|
692
|
+
const hostPath = selection.rootConfigured ? TVOS_ROOT_HOST_PATH : TVOS_LEGACY_HOST_PATH;
|
|
693
|
+
const absoluteHostPath = path_1.default.join(cwd, hostPath);
|
|
694
|
+
if (!fs_1.default.existsSync(absoluteHostPath)) {
|
|
695
|
+
return [
|
|
696
|
+
`Apple TV target hosanna-apple selects ${hostPath}, but that host file does not exist. Restore the selected host or update ${APPLE_PROJECT_PATH} to compile the other supported host; then rerun \`hst native:doctor apple\`.`,
|
|
697
|
+
];
|
|
698
|
+
}
|
|
699
|
+
const hostContent = fs_1.default.readFileSync(absoluteHostPath, 'utf8');
|
|
700
|
+
if (!satisfiesTvOsDebugOverlayCaptureContract(hostContent)) {
|
|
701
|
+
const mergeGuidance = selection.rootConfigured
|
|
702
|
+
? `Manually merge the current HosannaTvApp.swift template's per-UIPress debugOverlayCaptureRouteByPress routing into the compiled host.`
|
|
703
|
+
: `Merge per-UIPress debug-overlay routing into RootViewController.swift: Play/Pause and captured presses must go directly to RemoteProcessor without entering super.presses* or pressesForJS, and pressesCancelled must release the latched route.`;
|
|
704
|
+
return [
|
|
705
|
+
`Apple TV debug-overlay input capture is incomplete in the host actually compiled by ${APPLE_PROJECT_PATH}: ${hostPath}. ${mergeGuidance} This keeps Play/Pause remote-only and prevents a capture change during a five-second Play/Pause hold from splitting the UIKit lifecycle; then rerun \`hst native:doctor apple\`.`,
|
|
706
|
+
];
|
|
707
|
+
}
|
|
708
|
+
return [];
|
|
709
|
+
}
|
|
710
|
+
function satisfiesTvOsDebugOverlayCaptureContract(content) {
|
|
711
|
+
const routeFunction = swiftBracedSlice(content, 'func routePresses');
|
|
712
|
+
const beginsLifecycleBranch = routeFunction
|
|
713
|
+
? swiftBracedSlice(routeFunction, 'if beginsLifecycle')
|
|
714
|
+
: undefined;
|
|
715
|
+
if (!(beginsLifecycleBranch === null || beginsLifecycleBranch === void 0 ? void 0 : beginsLifecycleBranch.includes('debugOverlayCaptureRouteByPress[identifier] = routeToDebugOverlay'))) {
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
const requiredFragments = [
|
|
719
|
+
{ value: 'private var debugOverlayCaptureRouteByPress: [ObjectIdentifier: Bool] = [:]' },
|
|
720
|
+
{ value: 'routePresses(presses, beginsLifecycle: true)' },
|
|
721
|
+
{ value: 'routePresses(presses, beginsLifecycle: false)', count: 2 },
|
|
722
|
+
{ value: 'press.type == .playPause' },
|
|
723
|
+
{ value: 'RemoteProcessor.isDebugOverlayInputCaptured' },
|
|
724
|
+
{ value: 'debugOverlayCaptureRouteByPress[identifier] = routeToDebugOverlay' },
|
|
725
|
+
{ value: 'debugOverlayCaptureRouteByPress.removeValue(forKey: identifier)' },
|
|
726
|
+
{ value: 'override func pressesCancelled' },
|
|
727
|
+
];
|
|
728
|
+
if (!requiredFragments.every(fragment => {
|
|
729
|
+
var _a;
|
|
730
|
+
return (countOccurrences(content, fragment.value) >= ((_a = fragment.count) !== null && _a !== void 0 ? _a : 1));
|
|
731
|
+
})) {
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
return [
|
|
735
|
+
{
|
|
736
|
+
name: 'pressesBegan',
|
|
737
|
+
beginsLifecycle: true,
|
|
738
|
+
capturedDispatch: 'remoteProcessor?.handlePresses(routed.captured, pressed: true)',
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
name: 'pressesEnded',
|
|
742
|
+
beginsLifecycle: false,
|
|
743
|
+
capturedDispatch: 'remoteProcessor?.handlePresses(routed.captured, pressed: false)',
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
name: 'pressesCancelled',
|
|
747
|
+
beginsLifecycle: false,
|
|
748
|
+
capturedDispatch: 'remoteProcessor?.handlePressesCancelled(routed.captured)',
|
|
749
|
+
},
|
|
750
|
+
].every(handler => tvOsPressHandlerSatisfiesCaptureContract(content, handler));
|
|
751
|
+
}
|
|
752
|
+
function tvOsPressHandlerSatisfiesCaptureContract(content, handler) {
|
|
753
|
+
const handlerSlice = swiftOverrideFunctionSlice(content, handler.name);
|
|
754
|
+
if (!handlerSlice)
|
|
755
|
+
return false;
|
|
756
|
+
const routeCall = `routePresses(presses, beginsLifecycle: ${handler.beginsLifecycle})`;
|
|
757
|
+
if (!handlerSlice.includes(routeCall) || !handlerSlice.includes(handler.capturedDispatch))
|
|
758
|
+
return false;
|
|
759
|
+
if (handler.name === 'pressesCancelled' && /\.handlePresses\s*\(/.test(handlerSlice))
|
|
760
|
+
return false;
|
|
761
|
+
// The captured set may only be inspected for emptiness and dispatched directly
|
|
762
|
+
// to RemoteProcessor. Any other use can leak it back into UIKit or native-control
|
|
763
|
+
// filtering, including union/mixed expressions that start with routed.normal.
|
|
764
|
+
const unaccountedCapturedUses = handlerSlice
|
|
765
|
+
.split(handler.capturedDispatch).join('')
|
|
766
|
+
.split('routed.captured.isEmpty').join('');
|
|
767
|
+
if (unaccountedCapturedUses.includes('routed.captured'))
|
|
768
|
+
return false;
|
|
769
|
+
const forwardedArguments = [
|
|
770
|
+
...firstArgumentsForCalls(handlerSlice, `super.${handler.name}`),
|
|
771
|
+
...firstArgumentsForCalls(handlerSlice, 'pressesForJS'),
|
|
772
|
+
];
|
|
773
|
+
return forwardedArguments.every(argument => !/\bpresses\b/.test(argument));
|
|
774
|
+
}
|
|
775
|
+
function swiftOverrideFunctionSlice(content, functionName) {
|
|
776
|
+
return swiftBracedSlice(content, `override func ${functionName}`);
|
|
777
|
+
}
|
|
778
|
+
function swiftBracedSlice(content, marker) {
|
|
779
|
+
const functionStart = content.indexOf(marker);
|
|
780
|
+
if (functionStart < 0)
|
|
781
|
+
return undefined;
|
|
782
|
+
const bodyStart = content.indexOf('{', functionStart + marker.length);
|
|
783
|
+
if (bodyStart < 0)
|
|
784
|
+
return undefined;
|
|
785
|
+
let depth = 0;
|
|
786
|
+
for (let index = bodyStart; index < content.length; index += 1) {
|
|
787
|
+
if (content[index] === '{') {
|
|
788
|
+
depth += 1;
|
|
789
|
+
}
|
|
790
|
+
else if (content[index] === '}') {
|
|
791
|
+
depth -= 1;
|
|
792
|
+
if (depth === 0)
|
|
793
|
+
return content.slice(functionStart, index + 1);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return undefined;
|
|
797
|
+
}
|
|
798
|
+
function firstArgumentsForCalls(content, callName) {
|
|
799
|
+
var _a;
|
|
800
|
+
const argumentsFound = [];
|
|
801
|
+
let searchFrom = 0;
|
|
802
|
+
while (searchFrom < content.length) {
|
|
803
|
+
const callStart = content.indexOf(callName, searchFrom);
|
|
804
|
+
if (callStart < 0)
|
|
805
|
+
break;
|
|
806
|
+
let openingParenthesis = callStart + callName.length;
|
|
807
|
+
while (/\s/.test((_a = content[openingParenthesis]) !== null && _a !== void 0 ? _a : ''))
|
|
808
|
+
openingParenthesis += 1;
|
|
809
|
+
if (content[openingParenthesis] !== '(') {
|
|
810
|
+
searchFrom = openingParenthesis;
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
let nestedParentheses = 0;
|
|
814
|
+
let argumentEnd = -1;
|
|
815
|
+
for (let index = openingParenthesis + 1; index < content.length; index += 1) {
|
|
816
|
+
if (content[index] === '(') {
|
|
817
|
+
nestedParentheses += 1;
|
|
818
|
+
}
|
|
819
|
+
else if (content[index] === ')') {
|
|
820
|
+
if (nestedParentheses === 0) {
|
|
821
|
+
argumentEnd = index;
|
|
822
|
+
break;
|
|
823
|
+
}
|
|
824
|
+
nestedParentheses -= 1;
|
|
825
|
+
}
|
|
826
|
+
else if (content[index] === ',' && nestedParentheses === 0) {
|
|
827
|
+
argumentEnd = index;
|
|
828
|
+
break;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (argumentEnd < 0)
|
|
832
|
+
break;
|
|
833
|
+
argumentsFound.push(content.slice(openingParenthesis + 1, argumentEnd).trim());
|
|
834
|
+
searchFrom = argumentEnd + 1;
|
|
835
|
+
}
|
|
836
|
+
return argumentsFound;
|
|
837
|
+
}
|
|
838
|
+
function readAppleTvHostSelection(cwd) {
|
|
839
|
+
const projectPath = path_1.default.join(cwd, APPLE_PROJECT_PATH);
|
|
840
|
+
if (!fs_1.default.existsSync(projectPath)) {
|
|
841
|
+
return { projectExists: false, rootConfigured: false, legacyConfigured: false };
|
|
842
|
+
}
|
|
843
|
+
const entries = appleTvSourceEntries(fs_1.default.readFileSync(projectPath, 'utf8'));
|
|
844
|
+
const legacyEntryPointExists = fs_1.default.existsSync(path_1.default.join(cwd, TVOS_LEGACY_ENTRY_POINT_PATH));
|
|
845
|
+
const legacyConfigured = entries.some(entry => (sourceEntryCompilesFile(entry, TVOS_LEGACY_ENTRY_POINT_SOURCE_PATH)
|
|
846
|
+
&& (legacyEntryPointExists || explicitlySelectsLegacyHost(entry.path))));
|
|
847
|
+
return {
|
|
848
|
+
projectExists: true,
|
|
849
|
+
rootConfigured: entries.some(entry => sourceEntryCompilesFile(entry, TVOS_ROOT_HOST_SOURCE_PATH)),
|
|
850
|
+
legacyConfigured,
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
function explicitlySelectsLegacyHost(sourcePath) {
|
|
854
|
+
const normalized = normalizeAppleSourcePath(sourcePath);
|
|
855
|
+
return normalized === 'hosanna-apple/src/app'
|
|
856
|
+
|| normalized.startsWith('hosanna-apple/src/app/');
|
|
857
|
+
}
|
|
858
|
+
function appleTvSourceEntries(content) {
|
|
859
|
+
const targetLines = appleTvTargetBlock(content).split(/\r?\n/);
|
|
860
|
+
const sourcesIndex = targetLines.findIndex(line => /^\s*sources:\s*(?:#.*)?$/.test(line));
|
|
861
|
+
if (sourcesIndex < 0)
|
|
862
|
+
return [];
|
|
863
|
+
const sourcesIndent = indentation(targetLines[sourcesIndex]);
|
|
864
|
+
const sourceBlock = [];
|
|
865
|
+
for (let index = sourcesIndex + 1; index < targetLines.length; index += 1) {
|
|
866
|
+
const line = targetLines[index];
|
|
867
|
+
if (line.trim() && !line.trimStart().startsWith('#') && indentation(line) <= sourcesIndent)
|
|
868
|
+
break;
|
|
869
|
+
sourceBlock.push(line);
|
|
870
|
+
}
|
|
871
|
+
const candidateRows = sourceBlock
|
|
872
|
+
.map((line, index) => ({ line, index, indent: indentation(line) }))
|
|
873
|
+
.filter(row => /^\s*-\s+(?:path:\s*)?[^#\s]/.test(row.line) && row.indent > sourcesIndent);
|
|
874
|
+
if (candidateRows.length === 0)
|
|
875
|
+
return [];
|
|
876
|
+
const entryIndent = Math.min(...candidateRows.map(row => row.indent));
|
|
877
|
+
const entryRows = candidateRows.filter(row => row.indent === entryIndent);
|
|
878
|
+
return entryRows.flatMap((row, entryIndex) => {
|
|
879
|
+
var _a, _b;
|
|
880
|
+
const nextIndex = (_b = (_a = entryRows[entryIndex + 1]) === null || _a === void 0 ? void 0 : _a.index) !== null && _b !== void 0 ? _b : sourceBlock.length;
|
|
881
|
+
const entryBlock = sourceBlock.slice(row.index, nextIndex);
|
|
882
|
+
const sourcePath = parseSourceEntryPath(row.line);
|
|
883
|
+
if (!sourcePath)
|
|
884
|
+
return [];
|
|
885
|
+
return [{ path: sourcePath, excludes: parseSourceEntryExcludes(entryBlock) }];
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
function parseSourceEntryPath(line) {
|
|
889
|
+
const value = line.replace(/^\s*-\s+/, '');
|
|
890
|
+
const pathValue = value.startsWith('path:') ? value.slice('path:'.length) : value;
|
|
891
|
+
const parsed = parseYamlScalar(pathValue);
|
|
892
|
+
return parsed && !parsed.includes(':') ? parsed : undefined;
|
|
893
|
+
}
|
|
894
|
+
function parseSourceEntryExcludes(lines) {
|
|
895
|
+
const excludesIndex = lines.findIndex(line => /^\s*excludes:\s*(?:#.*)?$/.test(line));
|
|
896
|
+
if (excludesIndex < 0)
|
|
897
|
+
return [];
|
|
898
|
+
const excludesIndent = indentation(lines[excludesIndex]);
|
|
899
|
+
const excludes = [];
|
|
900
|
+
for (let index = excludesIndex + 1; index < lines.length; index += 1) {
|
|
901
|
+
const line = lines[index];
|
|
902
|
+
if (line.trim() && !line.trimStart().startsWith('#') && indentation(line) <= excludesIndent)
|
|
903
|
+
break;
|
|
904
|
+
const match = /^\s*-\s+(.+?)\s*$/.exec(line);
|
|
905
|
+
if (!match)
|
|
906
|
+
continue;
|
|
907
|
+
const value = parseYamlScalar(match[1]);
|
|
908
|
+
if (value)
|
|
909
|
+
excludes.push(value);
|
|
910
|
+
}
|
|
911
|
+
return excludes;
|
|
912
|
+
}
|
|
913
|
+
function parseYamlScalar(value) {
|
|
914
|
+
let normalized = value.trim();
|
|
915
|
+
if (!normalized)
|
|
916
|
+
return undefined;
|
|
917
|
+
const quote = normalized[0];
|
|
918
|
+
if ((quote === '"' || quote === "'") && normalized.lastIndexOf(quote) > 0) {
|
|
919
|
+
return normalized.slice(1, normalized.lastIndexOf(quote));
|
|
920
|
+
}
|
|
921
|
+
const commentIndex = normalized.indexOf(' #');
|
|
922
|
+
if (commentIndex >= 0)
|
|
923
|
+
normalized = normalized.slice(0, commentIndex).trimEnd();
|
|
924
|
+
return normalized || undefined;
|
|
925
|
+
}
|
|
926
|
+
function sourceEntryCompilesFile(entry, sourceFile) {
|
|
927
|
+
const sourcePath = normalizeAppleSourcePath(entry.path);
|
|
928
|
+
const normalizedFile = normalizeAppleSourcePath(sourceFile);
|
|
929
|
+
const relativeFile = normalizedFile === sourcePath
|
|
930
|
+
? path_1.default.posix.basename(normalizedFile)
|
|
931
|
+
: normalizedFile.startsWith(`${sourcePath}/`)
|
|
932
|
+
? normalizedFile.slice(sourcePath.length + 1)
|
|
933
|
+
: undefined;
|
|
934
|
+
if (relativeFile === undefined)
|
|
935
|
+
return false;
|
|
936
|
+
return !entry.excludes.some(pattern => (0, minimatch_1.minimatch)(relativeFile, normalizeAppleSourcePath(pattern), { dot: true }));
|
|
937
|
+
}
|
|
938
|
+
function normalizeAppleSourcePath(value) {
|
|
939
|
+
return value.replaceAll('\\', '/').replace(/^\.\//, '').replace(/\/$/, '');
|
|
940
|
+
}
|
|
941
|
+
function appleTvTargetBlock(content) {
|
|
942
|
+
const lines = content.split(/\r?\n/);
|
|
943
|
+
const targetIndex = lines.findIndex(line => /^\s+hosanna-apple:\s*(?:#.*)?$/.test(line));
|
|
944
|
+
if (targetIndex < 0)
|
|
945
|
+
return '';
|
|
946
|
+
const targetIndent = indentation(lines[targetIndex]);
|
|
947
|
+
let endIndex = lines.length;
|
|
948
|
+
for (let index = targetIndex + 1; index < lines.length; index += 1) {
|
|
949
|
+
const line = lines[index];
|
|
950
|
+
if (line.trim() && !line.trimStart().startsWith('#') && indentation(line) <= targetIndent) {
|
|
951
|
+
endIndex = index;
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return lines.slice(targetIndex, endIndex).join('\n');
|
|
956
|
+
}
|
|
957
|
+
function indentation(line) {
|
|
958
|
+
var _a, _b;
|
|
959
|
+
return (_b = (_a = /^\s*/.exec(line)) === null || _a === void 0 ? void 0 : _a[0].length) !== null && _b !== void 0 ? _b : 0;
|
|
960
|
+
}
|
|
646
961
|
function appendAndroidAutoMediaPreservationWarnings(projectFiles, warnings) {
|
|
647
962
|
const configuredFiles = [
|
|
648
963
|
[
|