@choochmeque/tauri-apple-extensions 0.1.2 → 0.2.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/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,43 +162,31 @@ 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(appGroupId) {
189
+ return `<?xml version="1.0" encoding="UTF-8"?>
197
190
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
198
191
  <plist version="1.0">
199
192
  <dict>
@@ -203,6 +196,25 @@ function createExtensionEntitlements(extensionDir, appGroupId) {
203
196
  </array>
204
197
  </dict>
205
198
  </plist>`;
199
+ }
200
+ function updateMainAppEntitlements(appleDir, appInfo, platform) {
201
+ const platformSuffix = platform === "ios" ? "iOS" : "macOS";
202
+ const targetName = `${appInfo.productName}_${platformSuffix}`;
203
+ const entitlementsPath = path.join(appleDir, targetName, `${targetName}.entitlements`);
204
+ const appGroupId = `group.${appInfo.identifier}`;
205
+ let entitlements;
206
+ if (fs.existsSync(entitlementsPath)) {
207
+ entitlements = fs.readFileSync(entitlementsPath, "utf8");
208
+ }
209
+ else {
210
+ entitlements = EMPTY_ENTITLEMENTS;
211
+ }
212
+ entitlements = addAppGroupToEntitlements(entitlements, appGroupId);
213
+ fs.writeFileSync(entitlementsPath, entitlements);
214
+ console.log(`Updated main app entitlements: ${entitlementsPath}`);
215
+ }
216
+ function createExtensionEntitlements(extensionDir, appGroupId) {
217
+ const entitlements = createEntitlementsContent(appGroupId);
206
218
  const entitlementsPath = path.join(extensionDir, `${path.basename(extensionDir)}.entitlements`);
207
219
  fs.writeFileSync(entitlementsPath, entitlements);
208
220
  return entitlementsPath;
@@ -253,7 +265,7 @@ const shareExtension = {
253
265
  extensionName(appInfo) {
254
266
  return `${appInfo.productName}-ShareExtension`;
255
267
  },
256
- createFiles(appleDir, appInfo, templatesDir) {
268
+ createFiles(appleDir, appInfo, templatesDir, _platform) {
257
269
  const extensionDir = path.join(appleDir, "ShareExtension");
258
270
  const appGroupId = `group.${appInfo.identifier}`;
259
271
  const urlScheme = appInfo.productName
@@ -346,16 +358,19 @@ const shareExtension = {
346
358
  createExtensionEntitlements(extensionDir, appGroupId);
347
359
  console.log(`Created ShareExtension files in ${extensionDir}`);
348
360
  },
349
- updateProjectYml(projectYml, appInfo) {
361
+ updateProjectYml(projectYml, appInfo, platform) {
350
362
  const extensionName = this.extensionName(appInfo);
351
363
  const extensionBundleId = `${appInfo.identifier}.ShareExtension`;
352
- const targetName = `${appInfo.productName}_iOS`;
364
+ const platformSuffix = platform === "ios" ? "iOS" : "macOS";
365
+ const platformValue = platform === "ios" ? "iOS" : "macOS";
366
+ const deploymentTarget = platform === "ios" ? "14.0" : "11.0";
367
+ const targetName = `${appInfo.productName}_${platformSuffix}`;
353
368
  // Create the extension target YAML
354
369
  const extensionTarget = `
355
370
  ${extensionName}:
356
371
  type: app-extension
357
- platform: iOS
358
- deploymentTarget: "14.0"
372
+ platform: ${platformValue}
373
+ deploymentTarget: "${deploymentTarget}"
359
374
  sources:
360
375
  - path: ShareExtension
361
376
  info:
@@ -404,6 +419,7 @@ const EXTENSIONS = {
404
419
  share: shareExtension,
405
420
  };
406
421
  function resolveTemplatesDir(options, extensionType) {
422
+ const { platform } = options;
407
423
  // Option 1: Explicit templates path
408
424
  if (options.templates) {
409
425
  const templatesPath = path.resolve(process.cwd(), options.templates);
@@ -423,16 +439,20 @@ function resolveTemplatesDir(options, extensionType) {
423
439
  if (extensionConfig.type !== extensionType) {
424
440
  throw new Error(`Plugin ${options.plugin} is for '${extensionConfig.type}' extension, not '${extensionType}'`);
425
441
  }
426
- const templatesPath = path.join(pluginPath, extensionConfig.templates);
442
+ const platformTemplates = extensionConfig.templates[platform];
443
+ if (!platformTemplates) {
444
+ throw new Error(`Plugin ${options.plugin} does not support ${platform} platform`);
445
+ }
446
+ const templatesPath = path.join(pluginPath, platformTemplates);
427
447
  if (!fs.existsSync(templatesPath)) {
428
448
  throw new Error(`Plugin templates directory not found: ${templatesPath}`);
429
449
  }
430
450
  return templatesPath;
431
451
  }
432
- // Option 3: Default templates (from bundled dist/cli.js -> ../templates)
433
- const defaultTemplates = path.join(__dirname$1, "../templates", extensionType);
452
+ // Option 3: Default templates (from bundled dist/cli.js -> ../templates/{platform}/{type})
453
+ const defaultTemplates = path.join(__dirname$1, "../templates", platform, extensionType);
434
454
  if (!fs.existsSync(defaultTemplates)) {
435
- throw new Error(`No templates found. Use --plugin or --templates to specify templates.`);
455
+ throw new Error(`No templates found for ${platform}/${extensionType}. Use --plugin or --templates to specify templates.`);
436
456
  }
437
457
  return defaultTemplates;
438
458
  }
@@ -450,7 +470,9 @@ function resolvePluginPath(pluginName) {
450
470
  throw new Error(`Plugin ${pluginName} not found in node_modules. Make sure it's installed.`);
451
471
  }
452
472
  async function addExtension(type, options) {
453
- console.log(`\nTauri Apple Extensions - Add ${type}\n`);
473
+ const { platform } = options;
474
+ const platformDisplay = platform === "ios" ? "iOS" : "macOS";
475
+ console.log(`\nTauri Apple Extensions - Add ${type} (${platformDisplay})\n`);
454
476
  try {
455
477
  // Validate extension type
456
478
  const extension = EXTENSIONS[type];
@@ -462,7 +484,7 @@ async function addExtension(type, options) {
462
484
  const projectRoot = findProjectRoot();
463
485
  console.log(`Project root: ${projectRoot}`);
464
486
  const tauriConfig = findTauriConfig(projectRoot);
465
- const appleDir = findAppleProjectDir(projectRoot);
487
+ const appleDir = findAppleProjectDir(projectRoot, platform);
466
488
  console.log(`Apple project dir: ${appleDir}`);
467
489
  // Get app info
468
490
  let projectYml = readProjectYml(appleDir);
@@ -482,12 +504,12 @@ async function addExtension(type, options) {
482
504
  console.log(`\nUsing templates from: ${templatesDir}`);
483
505
  // Run extension setup
484
506
  console.log(`\n1. Creating ${extension.displayName} files...`);
485
- extension.createFiles(appleDir, appInfo, templatesDir);
507
+ extension.createFiles(appleDir, appInfo, templatesDir, platform);
486
508
  console.log(`\n2. Updating main app entitlements...`);
487
- updateMainAppEntitlements(appleDir, appInfo);
509
+ updateMainAppEntitlements(appleDir, appInfo, platform);
488
510
  console.log(`\n3. Updating project.yml (extension target + URL scheme)...`);
489
511
  projectYml = readProjectYml(appleDir);
490
- projectYml = extension.updateProjectYml(projectYml, appInfo);
512
+ projectYml = extension.updateProjectYml(projectYml, appInfo, platform);
491
513
  writeProjectYml(appleDir, projectYml);
492
514
  console.log(`\n4. Regenerating Xcode project...`);
493
515
  runXcodeGen(appleDir);
@@ -512,12 +534,21 @@ async function addExtension(type, options) {
512
534
  const program = new Command();
513
535
  program
514
536
  .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);
537
+ .description("Add Apple extensions to Tauri apps")
538
+ .version("0.2.0");
539
+ function createPlatformCommand(platform) {
540
+ const cmd = new Command(platform);
541
+ cmd.description(`Manage ${platform === "ios" ? "iOS" : "macOS"} extensions`);
542
+ cmd
543
+ .command("add <type>")
544
+ .description("Add an extension (e.g., share)")
545
+ .option("-p, --plugin <name>", "Plugin to use for templates")
546
+ .option("-t, --templates <path>", "Custom templates directory")
547
+ .action((type, options) => {
548
+ addExtension(type, { ...options, platform });
549
+ });
550
+ return cmd;
551
+ }
552
+ program.addCommand(createPlatformCommand("ios"));
553
+ program.addCommand(createPlatformCommand("macos"));
523
554
  program.parse();
@@ -1,3 +1,6 @@
1
- import type { AppInfo } from "../types.js";
2
- export declare function updateMainAppEntitlements(appleDir: string, appInfo: AppInfo): void;
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(appGroupId: string): string;
5
+ export declare function updateMainAppEntitlements(appleDir: string, appInfo: AppInfo, platform: Platform): void;
3
6
  export declare function createExtensionEntitlements(extensionDir: string, appGroupId: string): 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,43 +160,31 @@ 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(appGroupId) {
187
+ return `<?xml version="1.0" encoding="UTF-8"?>
195
188
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
196
189
  <plist version="1.0">
197
190
  <dict>
@@ -201,6 +194,25 @@ function createExtensionEntitlements(extensionDir, appGroupId) {
201
194
  </array>
202
195
  </dict>
203
196
  </plist>`;
197
+ }
198
+ function updateMainAppEntitlements(appleDir, appInfo, platform) {
199
+ const platformSuffix = platform === "ios" ? "iOS" : "macOS";
200
+ const targetName = `${appInfo.productName}_${platformSuffix}`;
201
+ const entitlementsPath = path.join(appleDir, targetName, `${targetName}.entitlements`);
202
+ const appGroupId = `group.${appInfo.identifier}`;
203
+ let entitlements;
204
+ if (fs.existsSync(entitlementsPath)) {
205
+ entitlements = fs.readFileSync(entitlementsPath, "utf8");
206
+ }
207
+ else {
208
+ entitlements = EMPTY_ENTITLEMENTS;
209
+ }
210
+ entitlements = addAppGroupToEntitlements(entitlements, appGroupId);
211
+ fs.writeFileSync(entitlementsPath, entitlements);
212
+ console.log(`Updated main app entitlements: ${entitlementsPath}`);
213
+ }
214
+ function createExtensionEntitlements(extensionDir, appGroupId) {
215
+ const entitlements = createEntitlementsContent(appGroupId);
204
216
  const entitlementsPath = path.join(extensionDir, `${path.basename(extensionDir)}.entitlements`);
205
217
  fs.writeFileSync(entitlementsPath, entitlements);
206
218
  return entitlementsPath;
@@ -251,7 +263,7 @@ const shareExtension = {
251
263
  extensionName(appInfo) {
252
264
  return `${appInfo.productName}-ShareExtension`;
253
265
  },
254
- createFiles(appleDir, appInfo, templatesDir) {
266
+ createFiles(appleDir, appInfo, templatesDir, _platform) {
255
267
  const extensionDir = path.join(appleDir, "ShareExtension");
256
268
  const appGroupId = `group.${appInfo.identifier}`;
257
269
  const urlScheme = appInfo.productName
@@ -344,16 +356,19 @@ const shareExtension = {
344
356
  createExtensionEntitlements(extensionDir, appGroupId);
345
357
  console.log(`Created ShareExtension files in ${extensionDir}`);
346
358
  },
347
- updateProjectYml(projectYml, appInfo) {
359
+ updateProjectYml(projectYml, appInfo, platform) {
348
360
  const extensionName = this.extensionName(appInfo);
349
361
  const extensionBundleId = `${appInfo.identifier}.ShareExtension`;
350
- const targetName = `${appInfo.productName}_iOS`;
362
+ const platformSuffix = platform === "ios" ? "iOS" : "macOS";
363
+ const platformValue = platform === "ios" ? "iOS" : "macOS";
364
+ const deploymentTarget = platform === "ios" ? "14.0" : "11.0";
365
+ const targetName = `${appInfo.productName}_${platformSuffix}`;
351
366
  // Create the extension target YAML
352
367
  const extensionTarget = `
353
368
  ${extensionName}:
354
369
  type: app-extension
355
- platform: iOS
356
- deploymentTarget: "14.0"
370
+ platform: ${platformValue}
371
+ deploymentTarget: "${deploymentTarget}"
357
372
  sources:
358
373
  - path: ShareExtension
359
374
  info:
@@ -402,6 +417,7 @@ const EXTENSIONS = {
402
417
  share: shareExtension,
403
418
  };
404
419
  function resolveTemplatesDir(options, extensionType) {
420
+ const { platform } = options;
405
421
  // Option 1: Explicit templates path
406
422
  if (options.templates) {
407
423
  const templatesPath = path.resolve(process.cwd(), options.templates);
@@ -421,16 +437,20 @@ function resolveTemplatesDir(options, extensionType) {
421
437
  if (extensionConfig.type !== extensionType) {
422
438
  throw new Error(`Plugin ${options.plugin} is for '${extensionConfig.type}' extension, not '${extensionType}'`);
423
439
  }
424
- const templatesPath = path.join(pluginPath, extensionConfig.templates);
440
+ const platformTemplates = extensionConfig.templates[platform];
441
+ if (!platformTemplates) {
442
+ throw new Error(`Plugin ${options.plugin} does not support ${platform} platform`);
443
+ }
444
+ const templatesPath = path.join(pluginPath, platformTemplates);
425
445
  if (!fs.existsSync(templatesPath)) {
426
446
  throw new Error(`Plugin templates directory not found: ${templatesPath}`);
427
447
  }
428
448
  return templatesPath;
429
449
  }
430
- // Option 3: Default templates (from bundled dist/cli.js -> ../templates)
431
- const defaultTemplates = path.join(__dirname$1, "../templates", extensionType);
450
+ // Option 3: Default templates (from bundled dist/cli.js -> ../templates/{platform}/{type})
451
+ const defaultTemplates = path.join(__dirname$1, "../templates", platform, extensionType);
432
452
  if (!fs.existsSync(defaultTemplates)) {
433
- throw new Error(`No templates found. Use --plugin or --templates to specify templates.`);
453
+ throw new Error(`No templates found for ${platform}/${extensionType}. Use --plugin or --templates to specify templates.`);
434
454
  }
435
455
  return defaultTemplates;
436
456
  }
@@ -448,7 +468,9 @@ function resolvePluginPath(pluginName) {
448
468
  throw new Error(`Plugin ${pluginName} not found in node_modules. Make sure it's installed.`);
449
469
  }
450
470
  async function addExtension(type, options) {
451
- console.log(`\nTauri Apple Extensions - Add ${type}\n`);
471
+ const { platform } = options;
472
+ const platformDisplay = platform === "ios" ? "iOS" : "macOS";
473
+ console.log(`\nTauri Apple Extensions - Add ${type} (${platformDisplay})\n`);
452
474
  try {
453
475
  // Validate extension type
454
476
  const extension = EXTENSIONS[type];
@@ -460,7 +482,7 @@ async function addExtension(type, options) {
460
482
  const projectRoot = findProjectRoot();
461
483
  console.log(`Project root: ${projectRoot}`);
462
484
  const tauriConfig = findTauriConfig(projectRoot);
463
- const appleDir = findAppleProjectDir(projectRoot);
485
+ const appleDir = findAppleProjectDir(projectRoot, platform);
464
486
  console.log(`Apple project dir: ${appleDir}`);
465
487
  // Get app info
466
488
  let projectYml = readProjectYml(appleDir);
@@ -480,12 +502,12 @@ async function addExtension(type, options) {
480
502
  console.log(`\nUsing templates from: ${templatesDir}`);
481
503
  // Run extension setup
482
504
  console.log(`\n1. Creating ${extension.displayName} files...`);
483
- extension.createFiles(appleDir, appInfo, templatesDir);
505
+ extension.createFiles(appleDir, appInfo, templatesDir, platform);
484
506
  console.log(`\n2. Updating main app entitlements...`);
485
- updateMainAppEntitlements(appleDir, appInfo);
507
+ updateMainAppEntitlements(appleDir, appInfo, platform);
486
508
  console.log(`\n3. Updating project.yml (extension target + URL scheme)...`);
487
509
  projectYml = readProjectYml(appleDir);
488
- projectYml = extension.updateProjectYml(projectYml, appInfo);
510
+ projectYml = extension.updateProjectYml(projectYml, appInfo, platform);
489
511
  writeProjectYml(appleDir, projectYml);
490
512
  console.log(`\n4. Regenerating Xcode project...`);
491
513
  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,6 +1,6 @@
1
1
  {
2
2
  "name": "@choochmeque/tauri-apple-extensions",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Add iOS extensions to Tauri apps with a single command",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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