ReviewOS

also looking at this

craft-native/craft

feat(packaging): register the URL schemes an app declares

#57
Merged glennmichael123 wants to merge feat/macos-url-schemes into main
3 files +177 -7
packages/typescript/src/package.tsmodified+77-1
Changes to packages/typescript/src/package.ts
@@ -215,6 +215,21 @@ export interface PackageConfig {
215215 * the helper lands next to `process.execPath`.
216216 */
217217 additionalExecutables?: string[]
218
219 /**
220 * URL schemes the app registers as a handler for — `['myapp']` makes
221 * `myapp://…` open it.
222 *
223 * Emitted as `CFBundleURLTypes`. Without this key macOS never dispatches
224 * a URL to the app at all, so Craft's receive path (the `kAEGetURL`
225 * AppleEvent handler behind `craft.deepLink`) can be fully working and
226 * still never hear anything.
227 *
228 * Schemes are matched case-insensitively by LaunchServices and are
229 * global to the machine: pick something specific to the app, not `app`
230 * or `open`.
231 */
232 urlSchemes?: string[]
218233 }
219234
220235 /** Windows-specific options */
@@ -341,6 +356,7 @@ async function packageMacOS(config: PackageConfig, outDir: string): Promise<Pack
341356 category: opts.category,
342357 minimumSystemVersion: opts.minimumSystemVersion,
343358 menuBarOnly: opts.menuBarOnly,
359 urlSchemes: opts.urlSchemes,
344360 binaryPath: config.binaryPath,
345361 iconPath: config.iconPath,
346362 provisioningProfile: opts.provisioningProfile,
@@ -594,6 +610,60 @@ export interface MacOSBundleMetadata {
594610 minimumSystemVersion?: string
595611 /** Sets `LSUIElement` — no Dock icon, no app switcher entry */
596612 menuBarOnly?: boolean
613 /** URL schemes the app handles, emitted as `CFBundleURLTypes` */
614 urlSchemes?: string[]
615}
616
617/**
618 * A URL scheme, as RFC 3986 defines one: a letter, then letters, digits, `+`,
619 * `-` and `.`.
620 *
621 * Worth rejecting at package time rather than passing through, because the
622 * failure is otherwise invisible: LaunchServices ignores a malformed scheme,
623 * the app installs and runs, and links simply never arrive — which is the
624 * exact shape of bug this key exists to fix.
625 */
626const URL_SCHEME = /^[a-z][a-z0-9+.-]*$/i
627
628/**
629 * `CFBundleURLTypes` for the schemes an app handles, or `null` if it handles
630 * none.
631 *
632 * All the schemes go in one URL type. They are alternatives for reaching the
633 * same app, not different kinds of document, so splitting them across several
634 * dicts would only invent distinctions LaunchServices does not care about.
635 */
636export function urlTypesEntry(bundleId: string, schemes: readonly string[]): string | null {
637 const seen = new Set<string>()
638 const unique: string[] = []
639
640 for (const scheme of schemes) {
641 if (!URL_SCHEME.test(scheme))
642 throw new Error(`Invalid URL scheme ${JSON.stringify(scheme)}: must start with a letter and contain only letters, digits, "+", "-" or "."`)
643 // LaunchServices matches case-insensitively, so `MyApp` and `myapp` are
644 // one scheme declared twice.
645 const key = scheme.toLowerCase()
646 if (seen.has(key)) continue
647 seen.add(key)
648 unique.push(scheme)
649 }
650
651 if (unique.length === 0) return null
652
653 const list = unique.map(scheme => ` <string>${xml(scheme)}</string>`).join('\n')
654 return ` <key>CFBundleURLTypes</key>
655 <array>
656 <dict>
657 <key>CFBundleURLName</key>
658 <string>${xml(bundleId)}</string>
659 <key>CFBundleTypeRole</key>
660 <string>Viewer</string>
661 <key>CFBundleURLSchemes</key>
662 <array>
663${list}
664 </array>
665 </dict>
666 </array>`
597667}
598668
599669/**
@@ -618,10 +688,16 @@ export function macOSInfoPlist(metadata: MacOSBundleMetadata): string {
618688 if (metadata.minimumSystemVersion) entries.push(['LSMinimumSystemVersion', metadata.minimumSystemVersion])
619689 if (metadata.menuBarOnly) entries.push(['LSUIElement', true])
620690
621 const body = entries
691 const scalars = entries
622692 .map(([key, value]) => ` <key>${xml(key)}</key>\n${value === true ? ' <true/>' : ` <string>${xml(value)}</string>`}`)
623693 .join('\n')
624694
695 // Appended after the scalars rather than mixed in: it is the one entry whose
696 // value is not a string or a boolean, and keeping the simple mapping above
697 // simple is worth the extra line here.
698 const urlTypes = urlTypesEntry(metadata.bundleId, metadata.urlSchemes || [])
699 const body = urlTypes ? `${scalars}\n${urlTypes}` : scalars
700
625701 return `<?xml version="1.0" encoding="UTF-8"?>
626702<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
627703<plist version="1.0">