@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 +1 -1
- package/README.md +21 -7
- package/dist/cli.js +77 -46
- package/dist/core/entitlements.d.ts +5 -2
- package/dist/core/project-discovery.d.ts +2 -2
- package/dist/index.js +60 -38
- package/dist/types.d.ts +4 -2
- package/package.json +4 -1
- package/templates/macos/share/Info.plist +47 -0
- package/templates/macos/share/ShareViewController.swift +122 -0
- /package/templates/{share → ios/share}/Info.plist +0 -0
- /package/templates/{share → ios/share}/ShareViewController.swift +0 -0
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
# tauri-apple-extensions
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@choochmeque/tauri-apple-extensions)
|
|
4
|
+
[](https://codecov.io/gh/Choochmeque/tauri-apple-extensions)
|
|
4
5
|
[](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
|
-
|
|
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
|
|
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":
|
|
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",
|
|
50
|
-
path.join(projectRoot, "gen",
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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
|
|
196
|
-
|
|
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
|
|
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:
|
|
358
|
-
deploymentTarget: "
|
|
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
|
|
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
|
-
|
|
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
|
|
516
|
-
.version("0.
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
.description("
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
|
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",
|
|
48
|
-
path.join(projectRoot, "gen",
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
|
194
|
-
|
|
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
|
|
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:
|
|
356
|
-
deploymentTarget: "
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
File without changes
|