@choochmeque/tauri-apple-extensions 0.1.2 → 0.2.1

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Choochmeque
3
+ Copyright (c) 2025 Vladimir Pankratov
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,21 +1,24 @@
1
1
  # tauri-apple-extensions
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@choochmeque/tauri-apple-extensions.svg)](https://www.npmjs.com/package/@choochmeque/tauri-apple-extensions)
4
+ [![codecov](https://codecov.io/gh/Choochmeque/tauri-apple-extensions/branch/main/graph/badge.svg)](https://codecov.io/gh/Choochmeque/tauri-apple-extensions)
4
5
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
6
 
6
- Add iOS extensions to Tauri apps with a single command.
7
+ Add iOS and macOS extensions to Tauri apps with a single command.
7
8
 
8
9
  ## Features
9
10
 
10
11
  - Automatic Xcode project configuration via XcodeGen
11
12
  - Built-in skeleton templates with TODO markers
12
13
  - Plugin-based template system for custom implementations
14
+ - Supports both iOS and macOS platforms
13
15
  - Idempotent - safe to re-run
14
16
 
15
17
  ## Prerequisites
16
18
 
17
19
  - [XcodeGen](https://github.com/yonaskolb/XcodeGen) installed (`brew install xcodegen`)
18
- - Tauri iOS project initialized (`tauri ios init`)
20
+ - Tauri iOS project initialized (`tauri ios init`) for iOS extensions
21
+ - For macOS extensions: Tauri macOS Xcode project via [@choochmeque/tauri-macos-xcode](https://github.com/Choochmeque/tauri-macos-xcode)
19
22
 
20
23
  ## Installation
21
24
 
@@ -36,7 +39,11 @@ bun add -D @choochmeque/tauri-apple-extensions
36
39
  ## Usage
37
40
 
38
41
  ```bash
39
- npx @choochmeque/tauri-apple-extensions add share
42
+ # Add iOS Share Extension
43
+ npx @choochmeque/tauri-apple-extensions ios add share
44
+
45
+ # Add macOS Share Extension
46
+ npx @choochmeque/tauri-apple-extensions macos add share
40
47
  ```
41
48
 
42
49
  This creates a Share Extension with a minimal skeleton template. Open the generated Swift file and implement your logic where you see `// TODO:` comments.
@@ -45,10 +52,10 @@ This creates a Share Extension with a minimal skeleton template. Open the genera
45
52
 
46
53
  ```bash
47
54
  # Use templates from a plugin (plugin must include tauri-apple-extension config)
48
- npx @choochmeque/tauri-apple-extensions add share --plugin <plugin-name>
55
+ npx @choochmeque/tauri-apple-extensions ios add share --plugin <plugin-name>
49
56
 
50
57
  # Use custom templates directory
51
- npx @choochmeque/tauri-apple-extensions add share --templates ./path/to/templates
58
+ npx @choochmeque/tauri-apple-extensions ios add share --templates ./path/to/templates
52
59
  ```
53
60
 
54
61
  > **Note:** When using `--plugin`, the plugin's `package.json` must contain a `tauri-apple-extension` config. See [For Plugin Developers](#for-plugin-developers) below.
@@ -63,7 +70,9 @@ npx @choochmeque/tauri-apple-extensions add share --templates ./path/to/template
63
70
 
64
71
  After running the tool:
65
72
 
66
- 1. Open the Xcode project (`src-tauri/gen/apple/*.xcodeproj`)
73
+ 1. Open the Xcode project:
74
+ - iOS: `src-tauri/gen/apple/*.xcodeproj`
75
+ - macOS: `src-tauri/gen/apple-macos/*.xcodeproj`
67
76
  2. Select your Apple Developer Team for both targets
68
77
  3. Enable **App Groups** capability for both targets
69
78
  4. Configure App Groups in [Apple Developer Portal](https://developer.apple.com/account/resources/identifiers/list/applicationGroup)
@@ -76,11 +85,16 @@ To make your plugin compatible, add to your `package.json`:
76
85
  {
77
86
  "tauri-apple-extension": {
78
87
  "type": "share",
79
- "templates": "./ios/templates"
88
+ "templates": {
89
+ "ios": "./templates/ios",
90
+ "macos": "./templates/macos"
91
+ }
80
92
  }
81
93
  }
82
94
  ```
83
95
 
96
+ Plugins can support one or both platforms. If a user runs the command for a platform your plugin doesn't support, they'll receive a clear error message.
97
+
84
98
  ### Template Variables
85
99
 
86
100
  | Variable | Description |
package/dist/cli.js CHANGED
@@ -44,17 +44,22 @@ function findTauriConfig(projectRoot) {
44
44
  }
45
45
  throw new Error("Could not find tauri.conf.json");
46
46
  }
47
- function findAppleProjectDir(projectRoot) {
47
+ function findAppleProjectDir(projectRoot, platform) {
48
+ // iOS uses 'apple', macOS uses 'apple-macos'
49
+ const dirName = platform === "ios" ? "apple" : "apple-macos";
50
+ const initHint = platform === "ios"
51
+ ? "Run 'tauri ios init' first."
52
+ : "Set up macOS Xcode project using @choochmeque/tauri-macos-xcode first.";
48
53
  const paths = [
49
- path.join(projectRoot, "src-tauri", "gen", "apple"),
50
- path.join(projectRoot, "gen", "apple"),
54
+ path.join(projectRoot, "src-tauri", "gen", dirName),
55
+ path.join(projectRoot, "gen", dirName),
51
56
  ];
52
57
  for (const p of paths) {
53
58
  if (fs.existsSync(p)) {
54
59
  return p;
55
60
  }
56
61
  }
57
- throw new Error("Could not find iOS project directory. Run 'tauri ios init' first.");
62
+ throw new Error(`Could not find ${platform} project directory. ${initHint}`);
58
63
  }
59
64
  function getAppInfo(tauriConfig, projectYml) {
60
65
  const parsed = parseYamlSimple(projectYml);
@@ -157,52 +162,69 @@ function addDependencyToTarget(projectYml, mainTargetName, dependencyConfig) {
157
162
  return modified;
158
163
  }
159
164
 
160
- function updateMainAppEntitlements(appleDir, appInfo) {
161
- const targetName = `${appInfo.productName}_iOS`;
162
- const entitlementsPath = path.join(appleDir, targetName, `${targetName}.entitlements`);
163
- const appGroupId = `group.${appInfo.identifier}`;
164
- let entitlements;
165
- if (fs.existsSync(entitlementsPath)) {
166
- entitlements = fs.readFileSync(entitlementsPath, "utf8");
167
- }
168
- else {
169
- entitlements = `<?xml version="1.0" encoding="UTF-8"?>
165
+ const EMPTY_ENTITLEMENTS = `<?xml version="1.0" encoding="UTF-8"?>
170
166
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
171
167
  <plist version="1.0">
172
168
  <dict>
173
169
  </dict>
174
170
  </plist>`;
175
- }
171
+ function addAppGroupToEntitlements(entitlements, appGroupId) {
176
172
  // Check if app groups already configured
177
173
  if (entitlements.includes("com.apple.security.application-groups")) {
178
174
  if (!entitlements.includes(appGroupId)) {
179
175
  // Add our group to existing array
180
- entitlements = entitlements.replace(/(<key>com\.apple\.security\.application-groups<\/key>\s*<array>)/, `$1\n <string>${appGroupId}</string>`);
176
+ return entitlements.replace(/(<key>com\.apple\.security\.application-groups<\/key>\s*<array>)/, `$1\n <string>${appGroupId}</string>`);
181
177
  }
178
+ return entitlements;
182
179
  }
183
- else {
184
- // Add app groups entitlement
185
- entitlements = entitlements.replace(/<dict>\s*<\/dict>/, `<dict>
180
+ // Add app groups entitlement
181
+ return entitlements.replace(/<dict>\s*<\/dict>/, `<dict>
186
182
  <key>com.apple.security.application-groups</key>
187
183
  <array>
188
184
  <string>${appGroupId}</string>
189
185
  </array>
190
186
  </dict>`);
191
- }
192
- fs.writeFileSync(entitlementsPath, entitlements);
193
- console.log(`Updated main app entitlements: ${entitlementsPath}`);
194
187
  }
195
- function createExtensionEntitlements(extensionDir, appGroupId) {
196
- const entitlements = `<?xml version="1.0" encoding="UTF-8"?>
188
+ function createEntitlementsContent(platform, appGroupId) {
189
+ const entries = [];
190
+ // macOS extensions require app-sandbox to be registered
191
+ if (platform === "macos") {
192
+ entries.push(` <key>com.apple.security.app-sandbox</key>
193
+ <true/>`);
194
+ }
195
+ // App groups only if needed
196
+ if (appGroupId) {
197
+ entries.push(` <key>com.apple.security.application-groups</key>
198
+ <array>
199
+ <string>${appGroupId}</string>
200
+ </array>`);
201
+ }
202
+ return `<?xml version="1.0" encoding="UTF-8"?>
197
203
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
198
204
  <plist version="1.0">
199
205
  <dict>
200
- <key>com.apple.security.application-groups</key>
201
- <array>
202
- <string>${appGroupId}</string>
203
- </array>
206
+ ${entries.join("\n")}
204
207
  </dict>
205
208
  </plist>`;
209
+ }
210
+ function updateMainAppEntitlements(appleDir, appInfo, platform) {
211
+ const platformSuffix = platform === "ios" ? "iOS" : "macOS";
212
+ const targetName = `${appInfo.productName}_${platformSuffix}`;
213
+ const entitlementsPath = path.join(appleDir, targetName, `${targetName}.entitlements`);
214
+ const appGroupId = `group.${appInfo.identifier}`;
215
+ let entitlements;
216
+ if (fs.existsSync(entitlementsPath)) {
217
+ entitlements = fs.readFileSync(entitlementsPath, "utf8");
218
+ }
219
+ else {
220
+ entitlements = EMPTY_ENTITLEMENTS;
221
+ }
222
+ entitlements = addAppGroupToEntitlements(entitlements, appGroupId);
223
+ fs.writeFileSync(entitlementsPath, entitlements);
224
+ console.log(`Updated main app entitlements: ${entitlementsPath}`);
225
+ }
226
+ function createExtensionEntitlements(extensionDir, appGroupId, platform) {
227
+ const entitlements = createEntitlementsContent(platform, appGroupId);
206
228
  const entitlementsPath = path.join(extensionDir, `${path.basename(extensionDir)}.entitlements`);
207
229
  fs.writeFileSync(entitlementsPath, entitlements);
208
230
  return entitlementsPath;
@@ -253,7 +275,7 @@ const shareExtension = {
253
275
  extensionName(appInfo) {
254
276
  return `${appInfo.productName}-ShareExtension`;
255
277
  },
256
- createFiles(appleDir, appInfo, templatesDir) {
278
+ createFiles(appleDir, appInfo, templatesDir, platform) {
257
279
  const extensionDir = path.join(appleDir, "ShareExtension");
258
280
  const appGroupId = `group.${appInfo.identifier}`;
259
281
  const urlScheme = appInfo.productName
@@ -343,19 +365,22 @@ const shareExtension = {
343
365
  fs.writeFileSync(path.join(extensionDir, "Info.plist"), defaultInfoPlist);
344
366
  }
345
367
  // Create entitlements
346
- createExtensionEntitlements(extensionDir, appGroupId);
368
+ createExtensionEntitlements(extensionDir, appGroupId, platform);
347
369
  console.log(`Created ShareExtension files in ${extensionDir}`);
348
370
  },
349
- updateProjectYml(projectYml, appInfo) {
371
+ updateProjectYml(projectYml, appInfo, platform) {
350
372
  const extensionName = this.extensionName(appInfo);
351
373
  const extensionBundleId = `${appInfo.identifier}.ShareExtension`;
352
- const targetName = `${appInfo.productName}_iOS`;
374
+ const platformSuffix = platform === "ios" ? "iOS" : "macOS";
375
+ const platformValue = platform === "ios" ? "iOS" : "macOS";
376
+ const deploymentTarget = platform === "ios" ? "14.0" : "11.0";
377
+ const targetName = `${appInfo.productName}_${platformSuffix}`;
353
378
  // Create the extension target YAML
354
379
  const extensionTarget = `
355
380
  ${extensionName}:
356
381
  type: app-extension
357
- platform: iOS
358
- deploymentTarget: "14.0"
382
+ platform: ${platformValue}
383
+ deploymentTarget: "${deploymentTarget}"
359
384
  sources:
360
385
  - path: ShareExtension
361
386
  info:
@@ -404,6 +429,7 @@ const EXTENSIONS = {
404
429
  share: shareExtension,
405
430
  };
406
431
  function resolveTemplatesDir(options, extensionType) {
432
+ const { platform } = options;
407
433
  // Option 1: Explicit templates path
408
434
  if (options.templates) {
409
435
  const templatesPath = path.resolve(process.cwd(), options.templates);
@@ -423,16 +449,20 @@ function resolveTemplatesDir(options, extensionType) {
423
449
  if (extensionConfig.type !== extensionType) {
424
450
  throw new Error(`Plugin ${options.plugin} is for '${extensionConfig.type}' extension, not '${extensionType}'`);
425
451
  }
426
- const templatesPath = path.join(pluginPath, extensionConfig.templates);
452
+ const platformTemplates = extensionConfig.templates[platform];
453
+ if (!platformTemplates) {
454
+ throw new Error(`Plugin ${options.plugin} does not support ${platform} platform`);
455
+ }
456
+ const templatesPath = path.join(pluginPath, platformTemplates);
427
457
  if (!fs.existsSync(templatesPath)) {
428
458
  throw new Error(`Plugin templates directory not found: ${templatesPath}`);
429
459
  }
430
460
  return templatesPath;
431
461
  }
432
- // Option 3: Default templates (from bundled dist/cli.js -> ../templates)
433
- const defaultTemplates = path.join(__dirname$1, "../templates", extensionType);
462
+ // Option 3: Default templates (from bundled dist/cli.js -> ../templates/{platform}/{type})
463
+ const defaultTemplates = path.join(__dirname$1, "../templates", platform, extensionType);
434
464
  if (!fs.existsSync(defaultTemplates)) {
435
- throw new Error(`No templates found. Use --plugin or --templates to specify templates.`);
465
+ throw new Error(`No templates found for ${platform}/${extensionType}. Use --plugin or --templates to specify templates.`);
436
466
  }
437
467
  return defaultTemplates;
438
468
  }
@@ -450,7 +480,9 @@ function resolvePluginPath(pluginName) {
450
480
  throw new Error(`Plugin ${pluginName} not found in node_modules. Make sure it's installed.`);
451
481
  }
452
482
  async function addExtension(type, options) {
453
- console.log(`\nTauri Apple Extensions - Add ${type}\n`);
483
+ const { platform } = options;
484
+ const platformDisplay = platform === "ios" ? "iOS" : "macOS";
485
+ console.log(`\nTauri Apple Extensions - Add ${type} (${platformDisplay})\n`);
454
486
  try {
455
487
  // Validate extension type
456
488
  const extension = EXTENSIONS[type];
@@ -462,7 +494,7 @@ async function addExtension(type, options) {
462
494
  const projectRoot = findProjectRoot();
463
495
  console.log(`Project root: ${projectRoot}`);
464
496
  const tauriConfig = findTauriConfig(projectRoot);
465
- const appleDir = findAppleProjectDir(projectRoot);
497
+ const appleDir = findAppleProjectDir(projectRoot, platform);
466
498
  console.log(`Apple project dir: ${appleDir}`);
467
499
  // Get app info
468
500
  let projectYml = readProjectYml(appleDir);
@@ -482,12 +514,12 @@ async function addExtension(type, options) {
482
514
  console.log(`\nUsing templates from: ${templatesDir}`);
483
515
  // Run extension setup
484
516
  console.log(`\n1. Creating ${extension.displayName} files...`);
485
- extension.createFiles(appleDir, appInfo, templatesDir);
517
+ extension.createFiles(appleDir, appInfo, templatesDir, platform);
486
518
  console.log(`\n2. Updating main app entitlements...`);
487
- updateMainAppEntitlements(appleDir, appInfo);
519
+ updateMainAppEntitlements(appleDir, appInfo, platform);
488
520
  console.log(`\n3. Updating project.yml (extension target + URL scheme)...`);
489
521
  projectYml = readProjectYml(appleDir);
490
- projectYml = extension.updateProjectYml(projectYml, appInfo);
522
+ projectYml = extension.updateProjectYml(projectYml, appInfo, platform);
491
523
  writeProjectYml(appleDir, projectYml);
492
524
  console.log(`\n4. Regenerating Xcode project...`);
493
525
  runXcodeGen(appleDir);
@@ -512,12 +544,21 @@ async function addExtension(type, options) {
512
544
  const program = new Command();
513
545
  program
514
546
  .name("tauri-apple-extensions")
515
- .description("Add iOS extensions to Tauri apps")
516
- .version("0.1.2");
517
- program
518
- .command("add <type>")
519
- .description("Add an extension (e.g., share)")
520
- .option("-p, --plugin <name>", "Plugin to use for templates")
521
- .option("-t, --templates <path>", "Custom templates directory")
522
- .action(addExtension);
547
+ .description("Add Apple extensions to Tauri apps")
548
+ .version("0.2.1");
549
+ function createPlatformCommand(platform) {
550
+ const cmd = new Command(platform);
551
+ cmd.description(`Manage ${platform === "ios" ? "iOS" : "macOS"} extensions`);
552
+ cmd
553
+ .command("add <type>")
554
+ .description("Add an extension (e.g., share)")
555
+ .option("-p, --plugin <name>", "Plugin to use for templates")
556
+ .option("-t, --templates <path>", "Custom templates directory")
557
+ .action((type, options) => {
558
+ addExtension(type, { ...options, platform });
559
+ });
560
+ return cmd;
561
+ }
562
+ program.addCommand(createPlatformCommand("ios"));
563
+ program.addCommand(createPlatformCommand("macos"));
523
564
  program.parse();
@@ -1,3 +1,6 @@
1
- import type { AppInfo } from "../types.js";
2
- export declare function updateMainAppEntitlements(appleDir: string, appInfo: AppInfo): void;
3
- export declare function createExtensionEntitlements(extensionDir: string, appGroupId: string): string;
1
+ import type { AppInfo, Platform } from "../types.js";
2
+ export declare const EMPTY_ENTITLEMENTS = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n</dict>\n</plist>";
3
+ export declare function addAppGroupToEntitlements(entitlements: string, appGroupId: string): string;
4
+ export declare function createEntitlementsContent(platform: Platform, appGroupId?: string): string;
5
+ export declare function updateMainAppEntitlements(appleDir: string, appInfo: AppInfo, platform: Platform): void;
6
+ export declare function createExtensionEntitlements(extensionDir: string, appGroupId: string, platform: Platform): string;
@@ -1,5 +1,5 @@
1
- import type { AppInfo, TauriConfig } from "../types.js";
1
+ import type { AppInfo, TauriConfig, Platform } from "../types.js";
2
2
  export declare function findProjectRoot(): string;
3
3
  export declare function findTauriConfig(projectRoot: string): TauriConfig;
4
- export declare function findAppleProjectDir(projectRoot: string): string;
4
+ export declare function findAppleProjectDir(projectRoot: string, platform: Platform): string;
5
5
  export declare function getAppInfo(tauriConfig: TauriConfig, projectYml: string): AppInfo;
package/dist/index.js CHANGED
@@ -42,17 +42,22 @@ function findTauriConfig(projectRoot) {
42
42
  }
43
43
  throw new Error("Could not find tauri.conf.json");
44
44
  }
45
- function findAppleProjectDir(projectRoot) {
45
+ function findAppleProjectDir(projectRoot, platform) {
46
+ // iOS uses 'apple', macOS uses 'apple-macos'
47
+ const dirName = platform === "ios" ? "apple" : "apple-macos";
48
+ const initHint = platform === "ios"
49
+ ? "Run 'tauri ios init' first."
50
+ : "Set up macOS Xcode project using @choochmeque/tauri-macos-xcode first.";
46
51
  const paths = [
47
- path.join(projectRoot, "src-tauri", "gen", "apple"),
48
- path.join(projectRoot, "gen", "apple"),
52
+ path.join(projectRoot, "src-tauri", "gen", dirName),
53
+ path.join(projectRoot, "gen", dirName),
49
54
  ];
50
55
  for (const p of paths) {
51
56
  if (fs.existsSync(p)) {
52
57
  return p;
53
58
  }
54
59
  }
55
- throw new Error("Could not find iOS project directory. Run 'tauri ios init' first.");
60
+ throw new Error(`Could not find ${platform} project directory. ${initHint}`);
56
61
  }
57
62
  function getAppInfo(tauriConfig, projectYml) {
58
63
  const parsed = parseYamlSimple(projectYml);
@@ -155,52 +160,69 @@ function addDependencyToTarget(projectYml, mainTargetName, dependencyConfig) {
155
160
  return modified;
156
161
  }
157
162
 
158
- function updateMainAppEntitlements(appleDir, appInfo) {
159
- const targetName = `${appInfo.productName}_iOS`;
160
- const entitlementsPath = path.join(appleDir, targetName, `${targetName}.entitlements`);
161
- const appGroupId = `group.${appInfo.identifier}`;
162
- let entitlements;
163
- if (fs.existsSync(entitlementsPath)) {
164
- entitlements = fs.readFileSync(entitlementsPath, "utf8");
165
- }
166
- else {
167
- entitlements = `<?xml version="1.0" encoding="UTF-8"?>
163
+ const EMPTY_ENTITLEMENTS = `<?xml version="1.0" encoding="UTF-8"?>
168
164
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
169
165
  <plist version="1.0">
170
166
  <dict>
171
167
  </dict>
172
168
  </plist>`;
173
- }
169
+ function addAppGroupToEntitlements(entitlements, appGroupId) {
174
170
  // Check if app groups already configured
175
171
  if (entitlements.includes("com.apple.security.application-groups")) {
176
172
  if (!entitlements.includes(appGroupId)) {
177
173
  // Add our group to existing array
178
- entitlements = entitlements.replace(/(<key>com\.apple\.security\.application-groups<\/key>\s*<array>)/, `$1\n <string>${appGroupId}</string>`);
174
+ return entitlements.replace(/(<key>com\.apple\.security\.application-groups<\/key>\s*<array>)/, `$1\n <string>${appGroupId}</string>`);
179
175
  }
176
+ return entitlements;
180
177
  }
181
- else {
182
- // Add app groups entitlement
183
- entitlements = entitlements.replace(/<dict>\s*<\/dict>/, `<dict>
178
+ // Add app groups entitlement
179
+ return entitlements.replace(/<dict>\s*<\/dict>/, `<dict>
184
180
  <key>com.apple.security.application-groups</key>
185
181
  <array>
186
182
  <string>${appGroupId}</string>
187
183
  </array>
188
184
  </dict>`);
189
- }
190
- fs.writeFileSync(entitlementsPath, entitlements);
191
- console.log(`Updated main app entitlements: ${entitlementsPath}`);
192
185
  }
193
- function createExtensionEntitlements(extensionDir, appGroupId) {
194
- const entitlements = `<?xml version="1.0" encoding="UTF-8"?>
186
+ function createEntitlementsContent(platform, appGroupId) {
187
+ const entries = [];
188
+ // macOS extensions require app-sandbox to be registered
189
+ if (platform === "macos") {
190
+ entries.push(` <key>com.apple.security.app-sandbox</key>
191
+ <true/>`);
192
+ }
193
+ // App groups only if needed
194
+ if (appGroupId) {
195
+ entries.push(` <key>com.apple.security.application-groups</key>
196
+ <array>
197
+ <string>${appGroupId}</string>
198
+ </array>`);
199
+ }
200
+ return `<?xml version="1.0" encoding="UTF-8"?>
195
201
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
196
202
  <plist version="1.0">
197
203
  <dict>
198
- <key>com.apple.security.application-groups</key>
199
- <array>
200
- <string>${appGroupId}</string>
201
- </array>
204
+ ${entries.join("\n")}
202
205
  </dict>
203
206
  </plist>`;
207
+ }
208
+ function updateMainAppEntitlements(appleDir, appInfo, platform) {
209
+ const platformSuffix = platform === "ios" ? "iOS" : "macOS";
210
+ const targetName = `${appInfo.productName}_${platformSuffix}`;
211
+ const entitlementsPath = path.join(appleDir, targetName, `${targetName}.entitlements`);
212
+ const appGroupId = `group.${appInfo.identifier}`;
213
+ let entitlements;
214
+ if (fs.existsSync(entitlementsPath)) {
215
+ entitlements = fs.readFileSync(entitlementsPath, "utf8");
216
+ }
217
+ else {
218
+ entitlements = EMPTY_ENTITLEMENTS;
219
+ }
220
+ entitlements = addAppGroupToEntitlements(entitlements, appGroupId);
221
+ fs.writeFileSync(entitlementsPath, entitlements);
222
+ console.log(`Updated main app entitlements: ${entitlementsPath}`);
223
+ }
224
+ function createExtensionEntitlements(extensionDir, appGroupId, platform) {
225
+ const entitlements = createEntitlementsContent(platform, appGroupId);
204
226
  const entitlementsPath = path.join(extensionDir, `${path.basename(extensionDir)}.entitlements`);
205
227
  fs.writeFileSync(entitlementsPath, entitlements);
206
228
  return entitlementsPath;
@@ -251,7 +273,7 @@ const shareExtension = {
251
273
  extensionName(appInfo) {
252
274
  return `${appInfo.productName}-ShareExtension`;
253
275
  },
254
- createFiles(appleDir, appInfo, templatesDir) {
276
+ createFiles(appleDir, appInfo, templatesDir, platform) {
255
277
  const extensionDir = path.join(appleDir, "ShareExtension");
256
278
  const appGroupId = `group.${appInfo.identifier}`;
257
279
  const urlScheme = appInfo.productName
@@ -341,19 +363,22 @@ const shareExtension = {
341
363
  fs.writeFileSync(path.join(extensionDir, "Info.plist"), defaultInfoPlist);
342
364
  }
343
365
  // Create entitlements
344
- createExtensionEntitlements(extensionDir, appGroupId);
366
+ createExtensionEntitlements(extensionDir, appGroupId, platform);
345
367
  console.log(`Created ShareExtension files in ${extensionDir}`);
346
368
  },
347
- updateProjectYml(projectYml, appInfo) {
369
+ updateProjectYml(projectYml, appInfo, platform) {
348
370
  const extensionName = this.extensionName(appInfo);
349
371
  const extensionBundleId = `${appInfo.identifier}.ShareExtension`;
350
- const targetName = `${appInfo.productName}_iOS`;
372
+ const platformSuffix = platform === "ios" ? "iOS" : "macOS";
373
+ const platformValue = platform === "ios" ? "iOS" : "macOS";
374
+ const deploymentTarget = platform === "ios" ? "14.0" : "11.0";
375
+ const targetName = `${appInfo.productName}_${platformSuffix}`;
351
376
  // Create the extension target YAML
352
377
  const extensionTarget = `
353
378
  ${extensionName}:
354
379
  type: app-extension
355
- platform: iOS
356
- deploymentTarget: "14.0"
380
+ platform: ${platformValue}
381
+ deploymentTarget: "${deploymentTarget}"
357
382
  sources:
358
383
  - path: ShareExtension
359
384
  info:
@@ -402,6 +427,7 @@ const EXTENSIONS = {
402
427
  share: shareExtension,
403
428
  };
404
429
  function resolveTemplatesDir(options, extensionType) {
430
+ const { platform } = options;
405
431
  // Option 1: Explicit templates path
406
432
  if (options.templates) {
407
433
  const templatesPath = path.resolve(process.cwd(), options.templates);
@@ -421,16 +447,20 @@ function resolveTemplatesDir(options, extensionType) {
421
447
  if (extensionConfig.type !== extensionType) {
422
448
  throw new Error(`Plugin ${options.plugin} is for '${extensionConfig.type}' extension, not '${extensionType}'`);
423
449
  }
424
- const templatesPath = path.join(pluginPath, extensionConfig.templates);
450
+ const platformTemplates = extensionConfig.templates[platform];
451
+ if (!platformTemplates) {
452
+ throw new Error(`Plugin ${options.plugin} does not support ${platform} platform`);
453
+ }
454
+ const templatesPath = path.join(pluginPath, platformTemplates);
425
455
  if (!fs.existsSync(templatesPath)) {
426
456
  throw new Error(`Plugin templates directory not found: ${templatesPath}`);
427
457
  }
428
458
  return templatesPath;
429
459
  }
430
- // Option 3: Default templates (from bundled dist/cli.js -> ../templates)
431
- const defaultTemplates = path.join(__dirname$1, "../templates", extensionType);
460
+ // Option 3: Default templates (from bundled dist/cli.js -> ../templates/{platform}/{type})
461
+ const defaultTemplates = path.join(__dirname$1, "../templates", platform, extensionType);
432
462
  if (!fs.existsSync(defaultTemplates)) {
433
- throw new Error(`No templates found. Use --plugin or --templates to specify templates.`);
463
+ throw new Error(`No templates found for ${platform}/${extensionType}. Use --plugin or --templates to specify templates.`);
434
464
  }
435
465
  return defaultTemplates;
436
466
  }
@@ -448,7 +478,9 @@ function resolvePluginPath(pluginName) {
448
478
  throw new Error(`Plugin ${pluginName} not found in node_modules. Make sure it's installed.`);
449
479
  }
450
480
  async function addExtension(type, options) {
451
- console.log(`\nTauri Apple Extensions - Add ${type}\n`);
481
+ const { platform } = options;
482
+ const platformDisplay = platform === "ios" ? "iOS" : "macOS";
483
+ console.log(`\nTauri Apple Extensions - Add ${type} (${platformDisplay})\n`);
452
484
  try {
453
485
  // Validate extension type
454
486
  const extension = EXTENSIONS[type];
@@ -460,7 +492,7 @@ async function addExtension(type, options) {
460
492
  const projectRoot = findProjectRoot();
461
493
  console.log(`Project root: ${projectRoot}`);
462
494
  const tauriConfig = findTauriConfig(projectRoot);
463
- const appleDir = findAppleProjectDir(projectRoot);
495
+ const appleDir = findAppleProjectDir(projectRoot, platform);
464
496
  console.log(`Apple project dir: ${appleDir}`);
465
497
  // Get app info
466
498
  let projectYml = readProjectYml(appleDir);
@@ -480,12 +512,12 @@ async function addExtension(type, options) {
480
512
  console.log(`\nUsing templates from: ${templatesDir}`);
481
513
  // Run extension setup
482
514
  console.log(`\n1. Creating ${extension.displayName} files...`);
483
- extension.createFiles(appleDir, appInfo, templatesDir);
515
+ extension.createFiles(appleDir, appInfo, templatesDir, platform);
484
516
  console.log(`\n2. Updating main app entitlements...`);
485
- updateMainAppEntitlements(appleDir, appInfo);
517
+ updateMainAppEntitlements(appleDir, appInfo, platform);
486
518
  console.log(`\n3. Updating project.yml (extension target + URL scheme)...`);
487
519
  projectYml = readProjectYml(appleDir);
488
- projectYml = extension.updateProjectYml(projectYml, appInfo);
520
+ projectYml = extension.updateProjectYml(projectYml, appInfo, platform);
489
521
  writeProjectYml(appleDir, projectYml);
490
522
  console.log(`\n4. Regenerating Xcode project...`);
491
523
  runXcodeGen(appleDir);
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export type Platform = "ios" | "macos";
1
2
  export interface AppInfo {
2
3
  productName: string;
3
4
  bundleIdPrefix: string;
@@ -18,8 +19,8 @@ export interface Extension {
18
19
  extensionSuffix: string;
19
20
  extensionPointIdentifier: string;
20
21
  extensionName(appInfo: AppInfo): string;
21
- createFiles(appleDir: string, appInfo: AppInfo, templatesDir: string): void;
22
- updateProjectYml(projectYml: string, appInfo: AppInfo): string;
22
+ createFiles(appleDir: string, appInfo: AppInfo, templatesDir: string, platform: Platform): void;
23
+ updateProjectYml(projectYml: string, appInfo: AppInfo, platform: Platform): string;
23
24
  }
24
25
  export interface TargetConfig {
25
26
  name: string;
@@ -31,5 +32,6 @@ export interface DependencyConfig {
31
32
  export interface AddOptions {
32
33
  plugin?: string;
33
34
  templates?: string;
35
+ platform: Platform;
34
36
  }
35
37
  export type TemplateVariables = Record<string, string>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@choochmeque/tauri-apple-extensions",
3
- "version": "0.1.2",
4
- "description": "Add iOS extensions to Tauri apps with a single command",
3
+ "version": "0.2.1",
4
+ "description": "Add iOS and macOS extensions to Tauri apps with a single command",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "bin": {
@@ -36,8 +36,10 @@
36
36
  },
37
37
  "devDependencies": {
38
38
  "@rollup/plugin-node-resolve": "^16.0.3",
39
+ "@rollup/plugin-replace": "^6.0.3",
39
40
  "@rollup/plugin-typescript": "^12.3.0",
40
41
  "@types/node": "^25.0.3",
42
+ "@vitest/coverage-v8": "^4.0.16",
41
43
  "eslint": "^9.0.0",
42
44
  "prettier": "^3.0.0",
43
45
  "rollup": "^4.0.0",
@@ -54,6 +56,7 @@
54
56
  "pretest": "pnpm build",
55
57
  "test": "vitest run",
56
58
  "test:watch": "vitest",
59
+ "test:coverage": "vitest run --coverage",
57
60
  "lint": "eslint .",
58
61
  "format": "prettier --write \"./**/*.{cjs,mjs,js,jsx,mts,ts,tsx,html,css,json}\"",
59
62
  "format:check": "prettier --check \"./**/*.{cjs,mjs,js,jsx,mts,ts,tsx,html,css,json}\""
@@ -0,0 +1,47 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>CFBundleDevelopmentRegion</key>
6
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
7
+ <key>CFBundleDisplayName</key>
8
+ <string>Share</string>
9
+ <key>CFBundleExecutable</key>
10
+ <string>$(EXECUTABLE_NAME)</string>
11
+ <key>CFBundleIdentifier</key>
12
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
13
+ <key>CFBundleInfoDictionaryVersion</key>
14
+ <string>6.0</string>
15
+ <key>CFBundleName</key>
16
+ <string>$(PRODUCT_NAME)</string>
17
+ <key>CFBundlePackageType</key>
18
+ <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
19
+ <key>CFBundleShortVersionString</key>
20
+ <string>{{VERSION}}</string>
21
+ <key>CFBundleVersion</key>
22
+ <string>{{VERSION}}</string>
23
+ <key>NSExtension</key>
24
+ <dict>
25
+ <key>NSExtensionAttributes</key>
26
+ <dict>
27
+ <key>NSExtensionActivationRule</key>
28
+ <dict>
29
+ <key>NSExtensionActivationSupportsFileWithMaxCount</key>
30
+ <integer>10</integer>
31
+ <key>NSExtensionActivationSupportsImageWithMaxCount</key>
32
+ <integer>10</integer>
33
+ <key>NSExtensionActivationSupportsMovieWithMaxCount</key>
34
+ <integer>10</integer>
35
+ <key>NSExtensionActivationSupportsText</key>
36
+ <true/>
37
+ <key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
38
+ <integer>1</integer>
39
+ </dict>
40
+ </dict>
41
+ <key>NSExtensionPointIdentifier</key>
42
+ <string>com.apple.share-services</string>
43
+ <key>NSExtensionPrincipalClass</key>
44
+ <string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
45
+ </dict>
46
+ </dict>
47
+ </plist>
@@ -0,0 +1,122 @@
1
+ import AppKit
2
+ import Social
3
+ import UniformTypeIdentifiers
4
+
5
+ class ShareViewController: NSViewController {
6
+
7
+ // MARK: - Configuration
8
+ private let appGroupIdentifier = "{{APP_GROUP_IDENTIFIER}}"
9
+ private let appURLScheme = "{{APP_URL_SCHEME}}"
10
+
11
+ // MARK: - Lifecycle
12
+
13
+ override func loadView() {
14
+ self.view = NSView(frame: NSRect(x: 0, y: 0, width: 400, height: 300))
15
+ }
16
+
17
+ override func viewDidLoad() {
18
+ super.viewDidLoad()
19
+ // TODO: Setup your UI here
20
+ }
21
+
22
+ override func viewDidAppear() {
23
+ super.viewDidAppear()
24
+ processSharedItems()
25
+ }
26
+
27
+ // MARK: - Share Processing
28
+
29
+ private func processSharedItems() {
30
+ guard let extensionItems = extensionContext?.inputItems as? [NSExtensionItem] else {
31
+ complete()
32
+ return
33
+ }
34
+
35
+ for item in extensionItems {
36
+ guard let attachments = item.attachments else { continue }
37
+
38
+ for attachment in attachments {
39
+ // TODO: Handle different content types
40
+ // Examples:
41
+ // - UTType.image.identifier for images
42
+ // - UTType.url.identifier for URLs
43
+ // - UTType.text.identifier for text
44
+ // - UTType.fileURL.identifier for files
45
+
46
+ if attachment.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
47
+ attachment.loadItem(forTypeIdentifier: UTType.url.identifier) { [weak self] item, error in
48
+ if let url = item as? URL {
49
+ // TODO: Process the URL
50
+ print("Received URL: \(url)")
51
+ }
52
+ DispatchQueue.main.async {
53
+ self?.complete()
54
+ }
55
+ }
56
+ return
57
+ }
58
+
59
+ if attachment.hasItemConformingToTypeIdentifier(UTType.text.identifier) {
60
+ attachment.loadItem(forTypeIdentifier: UTType.text.identifier) { [weak self] item, error in
61
+ if let text = item as? String {
62
+ // TODO: Process the text
63
+ print("Received text: \(text)")
64
+ }
65
+ DispatchQueue.main.async {
66
+ self?.complete()
67
+ }
68
+ }
69
+ return
70
+ }
71
+
72
+ if attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
73
+ attachment.loadItem(forTypeIdentifier: UTType.image.identifier) { [weak self] item, error in
74
+ if let url = item as? URL {
75
+ // TODO: Process image file URL
76
+ print("Received image: \(url)")
77
+ } else if let image = item as? NSImage {
78
+ // TODO: Process NSImage directly
79
+ print("Received NSImage")
80
+ }
81
+ DispatchQueue.main.async {
82
+ self?.complete()
83
+ }
84
+ }
85
+ return
86
+ }
87
+ }
88
+ }
89
+
90
+ complete()
91
+ }
92
+
93
+ // MARK: - App Group Storage (Optional)
94
+
95
+ /// Save data to App Group for main app to read
96
+ private func saveToAppGroup(_ data: Data, forKey key: String) -> Bool {
97
+ guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
98
+ print("App Groups not configured")
99
+ return false
100
+ }
101
+ userDefaults.set(data, forKey: key)
102
+ return true
103
+ }
104
+
105
+ /// Get App Group container URL for file storage
106
+ private func appGroupContainerURL() -> URL? {
107
+ FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
108
+ }
109
+
110
+ // MARK: - Open Main App (Optional)
111
+
112
+ private func openMainApp() {
113
+ guard let url = URL(string: "\(appURLScheme)://share") else { return }
114
+ NSWorkspace.shared.open(url)
115
+ }
116
+
117
+ // MARK: - Complete
118
+
119
+ private func complete() {
120
+ extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
121
+ }
122
+ }
File without changes