@choochmeque/tauri-apple-extensions 0.1.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 +21 -0
- package/README.md +90 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +536 -0
- package/dist/commands/add.d.ts +2 -0
- package/dist/core/entitlements.d.ts +3 -0
- package/dist/core/info-plist.d.ts +2 -0
- package/dist/core/project-discovery.d.ts +5 -0
- package/dist/core/project-yml.d.ts +5 -0
- package/dist/core/xcodegen.d.ts +1 -0
- package/dist/extensions/share.d.ts +2 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +523 -0
- package/dist/types.d.ts +35 -0
- package/dist/utils/plist.d.ts +11 -0
- package/dist/utils/template.d.ts +14 -0
- package/dist/utils/yaml-simple.d.ts +5 -0
- package/package.json +64 -0
- package/templates/share/Info.plist +47 -0
- package/templates/share/ShareViewController.swift +246 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Choochmeque
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# tauri-apple-extensions
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@choochmeque/tauri-apple-extensions)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
Add iOS extensions to Tauri apps with a single command.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- Automatic Xcode project configuration via XcodeGen
|
|
11
|
+
- Plugin-based template system
|
|
12
|
+
- Idempotent - safe to re-run
|
|
13
|
+
- Share Extension support (more extension types coming soon)
|
|
14
|
+
|
|
15
|
+
## Prerequisites
|
|
16
|
+
|
|
17
|
+
- [XcodeGen](https://github.com/yonaskolb/XcodeGen) installed (`brew install xcodegen`)
|
|
18
|
+
- Tauri iOS project initialized (`tauri ios init`)
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -D @choochmeque/tauri-apple-extensions
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
### With a plugin (recommended)
|
|
29
|
+
|
|
30
|
+
If you're using a Tauri plugin that provides iOS extension templates:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx @choochmeque/tauri-apple-extensions add share --plugin @choochmeque/tauri-plugin-sharekit-api
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### With custom templates
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx @choochmeque/tauri-apple-extensions add share --templates ./path/to/templates
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Supported Extensions
|
|
43
|
+
|
|
44
|
+
| Type | Status | Description |
|
|
45
|
+
|------|--------|-------------|
|
|
46
|
+
| `share` | Available | Share Extension for receiving shared content |
|
|
47
|
+
|
|
48
|
+
## For Plugin Developers
|
|
49
|
+
|
|
50
|
+
To make your plugin compatible with this tool, add the following to your `package.json`:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"tauri-apple-extension": {
|
|
55
|
+
"type": "share",
|
|
56
|
+
"templates": "./ios/templates"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Your templates directory should contain:
|
|
62
|
+
- Swift source files with `{{VARIABLE}}` placeholders
|
|
63
|
+
- `Info.plist` for the extension
|
|
64
|
+
|
|
65
|
+
### Template Variables
|
|
66
|
+
|
|
67
|
+
| Variable | Description |
|
|
68
|
+
|----------|-------------|
|
|
69
|
+
| `{{APP_GROUP_IDENTIFIER}}` | App Group ID (e.g., `group.com.example.app`) |
|
|
70
|
+
| `{{APP_URL_SCHEME}}` | URL scheme for deep linking |
|
|
71
|
+
| `{{VERSION}}` | App version from tauri.conf.json |
|
|
72
|
+
| `{{BUNDLE_IDENTIFIER}}` | Extension bundle identifier |
|
|
73
|
+
| `{{PRODUCT_NAME}}` | App product name |
|
|
74
|
+
|
|
75
|
+
## Post-Setup Steps
|
|
76
|
+
|
|
77
|
+
After running the tool:
|
|
78
|
+
|
|
79
|
+
1. Open the Xcode project (`src-tauri/gen/apple/*.xcodeproj`)
|
|
80
|
+
2. Select your Apple Developer Team for both targets
|
|
81
|
+
3. Enable required capabilities (e.g., App Groups) for both targets
|
|
82
|
+
4. Configure the capability in Apple Developer Portal
|
|
83
|
+
|
|
84
|
+
## Contributing
|
|
85
|
+
|
|
86
|
+
PRs accepted. Please make sure to read the Contributing Guide before making a pull request.
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Simple YAML-like parsing for the values we need.
|
|
10
|
+
* This is not a full YAML parser, just extracts top-level key-value pairs.
|
|
11
|
+
*/
|
|
12
|
+
function parseYamlSimple(content) {
|
|
13
|
+
const lines = content.split("\n");
|
|
14
|
+
const result = {};
|
|
15
|
+
for (const line of lines) {
|
|
16
|
+
const match = line.match(/^(\w+):\s*(.+)$/);
|
|
17
|
+
if (match) {
|
|
18
|
+
result[match[1]] = match[2].trim();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function findProjectRoot() {
|
|
25
|
+
let dir = process.cwd();
|
|
26
|
+
while (dir !== path.dirname(dir)) {
|
|
27
|
+
if (fs.existsSync(path.join(dir, "tauri.conf.json")) ||
|
|
28
|
+
fs.existsSync(path.join(dir, "src-tauri", "tauri.conf.json"))) {
|
|
29
|
+
return dir;
|
|
30
|
+
}
|
|
31
|
+
dir = path.dirname(dir);
|
|
32
|
+
}
|
|
33
|
+
return process.cwd();
|
|
34
|
+
}
|
|
35
|
+
function findTauriConfig(projectRoot) {
|
|
36
|
+
const paths = [
|
|
37
|
+
path.join(projectRoot, "src-tauri", "tauri.conf.json"),
|
|
38
|
+
path.join(projectRoot, "tauri.conf.json"),
|
|
39
|
+
];
|
|
40
|
+
for (const p of paths) {
|
|
41
|
+
if (fs.existsSync(p)) {
|
|
42
|
+
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
throw new Error("Could not find tauri.conf.json");
|
|
46
|
+
}
|
|
47
|
+
function findAppleProjectDir(projectRoot) {
|
|
48
|
+
const paths = [
|
|
49
|
+
path.join(projectRoot, "src-tauri", "gen", "apple"),
|
|
50
|
+
path.join(projectRoot, "gen", "apple"),
|
|
51
|
+
];
|
|
52
|
+
for (const p of paths) {
|
|
53
|
+
if (fs.existsSync(p)) {
|
|
54
|
+
return p;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
throw new Error("Could not find iOS project directory. Run 'tauri ios init' first.");
|
|
58
|
+
}
|
|
59
|
+
function getAppInfo(tauriConfig, projectYml) {
|
|
60
|
+
const parsed = parseYamlSimple(projectYml);
|
|
61
|
+
const productName = tauriConfig.productName || tauriConfig.package?.productName || "app";
|
|
62
|
+
const bundleIdPrefix = parsed.bundleIdPrefix || "com.tauri";
|
|
63
|
+
const identifier = tauriConfig.identifier || `${bundleIdPrefix}.${productName}`;
|
|
64
|
+
const version = tauriConfig.version || "1.0.0";
|
|
65
|
+
return {
|
|
66
|
+
productName,
|
|
67
|
+
bundleIdPrefix,
|
|
68
|
+
identifier,
|
|
69
|
+
version,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readProjectYml(appleDir) {
|
|
74
|
+
const projectYmlPath = path.join(appleDir, "project.yml");
|
|
75
|
+
if (!fs.existsSync(projectYmlPath)) {
|
|
76
|
+
throw new Error("project.yml not found. Run 'tauri ios init' first.");
|
|
77
|
+
}
|
|
78
|
+
return fs.readFileSync(projectYmlPath, "utf8");
|
|
79
|
+
}
|
|
80
|
+
function writeProjectYml(appleDir, content) {
|
|
81
|
+
const projectYmlPath = path.join(appleDir, "project.yml");
|
|
82
|
+
fs.writeFileSync(projectYmlPath, content);
|
|
83
|
+
}
|
|
84
|
+
function addExtensionTarget(projectYml, targetConfig) {
|
|
85
|
+
let modified = projectYml;
|
|
86
|
+
// Remove existing target if it exists (to allow re-running the script)
|
|
87
|
+
// Note: Don't use 'm' flag - we need $ to match only at true end of string
|
|
88
|
+
const existingTargetRegex = new RegExp(`\\n ${targetConfig.name}:[\\s\\S]*?(?=\\n [a-zA-Z_]|\\n[a-zA-Z]|$)`);
|
|
89
|
+
if (modified.match(existingTargetRegex)) {
|
|
90
|
+
console.log(`Removing existing ${targetConfig.name} target to recreate it...`);
|
|
91
|
+
modified = modified.replace(existingTargetRegex, "");
|
|
92
|
+
}
|
|
93
|
+
// Find the end of targets section and insert the new target
|
|
94
|
+
if (modified.includes("targets:")) {
|
|
95
|
+
const targetsIndex = modified.indexOf("targets:");
|
|
96
|
+
const afterTargets = modified.slice(targetsIndex);
|
|
97
|
+
const lines = afterTargets.split("\n");
|
|
98
|
+
let inTargets = false;
|
|
99
|
+
let lastTargetEnd = targetsIndex;
|
|
100
|
+
for (let i = 0; i < lines.length; i++) {
|
|
101
|
+
const line = lines[i];
|
|
102
|
+
if (line.match(/^targets:/)) {
|
|
103
|
+
inTargets = true;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (inTargets) {
|
|
107
|
+
// Check if we've exited the targets section (new top-level key)
|
|
108
|
+
if (line.match(/^[a-zA-Z]/) && !line.startsWith(" ")) {
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
lastTargetEnd = targetsIndex + lines.slice(0, i + 1).join("\n").length;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
modified =
|
|
115
|
+
modified.slice(0, lastTargetEnd) +
|
|
116
|
+
targetConfig.yaml +
|
|
117
|
+
modified.slice(lastTargetEnd);
|
|
118
|
+
}
|
|
119
|
+
return modified;
|
|
120
|
+
}
|
|
121
|
+
function addDependencyToTarget(projectYml, mainTargetName, dependencyConfig) {
|
|
122
|
+
let modified = projectYml;
|
|
123
|
+
// Remove existing dependency if it exists
|
|
124
|
+
const existingDepRegex = new RegExp(`\\n - target: ${dependencyConfig.target}\\n embed: true\\n codeSign: true`, "g");
|
|
125
|
+
modified = modified.replace(existingDepRegex, "");
|
|
126
|
+
// Find the main target's dependencies section and add the extension
|
|
127
|
+
const targetRegex = new RegExp(`(${mainTargetName}:[\\s\\S]*?dependencies:)([\\s\\S]*?)(\\n\\s{4}\\w|$)`, "m");
|
|
128
|
+
const depMatch = modified.match(targetRegex);
|
|
129
|
+
if (depMatch) {
|
|
130
|
+
const depSection = depMatch[2];
|
|
131
|
+
const newDep = `\n - target: ${dependencyConfig.target}
|
|
132
|
+
embed: true
|
|
133
|
+
codeSign: true`;
|
|
134
|
+
if (!depSection.includes(dependencyConfig.target)) {
|
|
135
|
+
modified = modified.replace(targetRegex, `$1${depSection}${newDep}$3`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return modified;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function updateMainAppEntitlements(appleDir, appInfo) {
|
|
142
|
+
const targetName = `${appInfo.productName}_iOS`;
|
|
143
|
+
const entitlementsPath = path.join(appleDir, targetName, `${targetName}.entitlements`);
|
|
144
|
+
const appGroupId = `group.${appInfo.identifier}`;
|
|
145
|
+
let entitlements;
|
|
146
|
+
if (fs.existsSync(entitlementsPath)) {
|
|
147
|
+
entitlements = fs.readFileSync(entitlementsPath, "utf8");
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
entitlements = `<?xml version="1.0" encoding="UTF-8"?>
|
|
151
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
152
|
+
<plist version="1.0">
|
|
153
|
+
<dict>
|
|
154
|
+
</dict>
|
|
155
|
+
</plist>`;
|
|
156
|
+
}
|
|
157
|
+
// Check if app groups already configured
|
|
158
|
+
if (entitlements.includes("com.apple.security.application-groups")) {
|
|
159
|
+
if (!entitlements.includes(appGroupId)) {
|
|
160
|
+
// Add our group to existing array
|
|
161
|
+
entitlements = entitlements.replace(/(<key>com\.apple\.security\.application-groups<\/key>\s*<array>)/, `$1\n <string>${appGroupId}</string>`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
// Add app groups entitlement
|
|
166
|
+
entitlements = entitlements.replace(/<dict>\s*<\/dict>/, `<dict>
|
|
167
|
+
<key>com.apple.security.application-groups</key>
|
|
168
|
+
<array>
|
|
169
|
+
<string>${appGroupId}</string>
|
|
170
|
+
</array>
|
|
171
|
+
</dict>`);
|
|
172
|
+
}
|
|
173
|
+
fs.writeFileSync(entitlementsPath, entitlements);
|
|
174
|
+
console.log(`Updated main app entitlements: ${entitlementsPath}`);
|
|
175
|
+
}
|
|
176
|
+
function createExtensionEntitlements(extensionDir, appGroupId) {
|
|
177
|
+
const entitlements = `<?xml version="1.0" encoding="UTF-8"?>
|
|
178
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
179
|
+
<plist version="1.0">
|
|
180
|
+
<dict>
|
|
181
|
+
<key>com.apple.security.application-groups</key>
|
|
182
|
+
<array>
|
|
183
|
+
<string>${appGroupId}</string>
|
|
184
|
+
</array>
|
|
185
|
+
</dict>
|
|
186
|
+
</plist>`;
|
|
187
|
+
const entitlementsPath = path.join(extensionDir, `${path.basename(extensionDir)}.entitlements`);
|
|
188
|
+
fs.writeFileSync(entitlementsPath, entitlements);
|
|
189
|
+
return entitlementsPath;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function addUrlSchemeToInfoPlist(appleDir, appInfo) {
|
|
193
|
+
const targetName = `${appInfo.productName}_iOS`;
|
|
194
|
+
const infoPlistPath = path.join(appleDir, targetName, "Info.plist");
|
|
195
|
+
const urlScheme = appInfo.productName.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
196
|
+
if (!fs.existsSync(infoPlistPath)) {
|
|
197
|
+
console.log(`Info.plist not found at ${infoPlistPath}, skipping URL scheme setup`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
let infoPlist = fs.readFileSync(infoPlistPath, "utf8");
|
|
201
|
+
// Check if URL schemes already configured
|
|
202
|
+
if (infoPlist.includes("CFBundleURLSchemes")) {
|
|
203
|
+
if (!infoPlist.includes(urlScheme)) {
|
|
204
|
+
console.log(`URL scheme may need manual configuration. Add '${urlScheme}' to CFBundleURLSchemes.`);
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
// Add URL scheme - need to insert before closing </dict></plist>
|
|
209
|
+
const urlSchemeEntry = ` <key>CFBundleURLTypes</key>
|
|
210
|
+
<array>
|
|
211
|
+
<dict>
|
|
212
|
+
<key>CFBundleURLSchemes</key>
|
|
213
|
+
<array>
|
|
214
|
+
<string>${urlScheme}</string>
|
|
215
|
+
</array>
|
|
216
|
+
<key>CFBundleURLName</key>
|
|
217
|
+
<string>${appInfo.identifier}</string>
|
|
218
|
+
</dict>
|
|
219
|
+
</array>
|
|
220
|
+
`;
|
|
221
|
+
// Insert before the last </dict>
|
|
222
|
+
infoPlist = infoPlist.replace(/(\s*)<\/dict>\s*<\/plist>/, `\n${urlSchemeEntry}$1</dict>\n</plist>`);
|
|
223
|
+
fs.writeFileSync(infoPlistPath, infoPlist);
|
|
224
|
+
console.log(`Added URL scheme '${urlScheme}' to Info.plist`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function runXcodeGen(appleDir) {
|
|
228
|
+
try {
|
|
229
|
+
console.log("Running xcodegen to regenerate project...");
|
|
230
|
+
execSync("xcodegen generate", {
|
|
231
|
+
cwd: appleDir,
|
|
232
|
+
stdio: "inherit",
|
|
233
|
+
});
|
|
234
|
+
console.log("Xcode project regenerated successfully!");
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
console.log("\nNote: xcodegen not found or failed. You may need to run it manually:");
|
|
238
|
+
console.log(` cd ${appleDir} && xcodegen generate`);
|
|
239
|
+
console.log(` Error: ${error.message}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Replace template variables in content.
|
|
245
|
+
* Variables are in the format {{VARIABLE_NAME}}
|
|
246
|
+
*/
|
|
247
|
+
function replaceTemplateVariables(content, variables) {
|
|
248
|
+
let result = content;
|
|
249
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
250
|
+
const regex = new RegExp(`\\{\\{${key}\\}\\}`, "g");
|
|
251
|
+
result = result.replace(regex, value);
|
|
252
|
+
}
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Copy a template file to destination, replacing variables.
|
|
257
|
+
*/
|
|
258
|
+
function copyTemplateFile(src, dest, variables) {
|
|
259
|
+
let content = fs.readFileSync(src, "utf8");
|
|
260
|
+
content = replaceTemplateVariables(content, variables);
|
|
261
|
+
fs.writeFileSync(dest, content);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const shareExtension = {
|
|
265
|
+
type: "share",
|
|
266
|
+
displayName: "Share Extension",
|
|
267
|
+
extensionSuffix: "ShareExtension",
|
|
268
|
+
extensionPointIdentifier: "com.apple.share-services",
|
|
269
|
+
extensionName(appInfo) {
|
|
270
|
+
return `${appInfo.productName}-ShareExtension`;
|
|
271
|
+
},
|
|
272
|
+
createFiles(appleDir, appInfo, templatesDir) {
|
|
273
|
+
const extensionDir = path.join(appleDir, "ShareExtension");
|
|
274
|
+
const appGroupId = `group.${appInfo.identifier}`;
|
|
275
|
+
const urlScheme = appInfo.productName
|
|
276
|
+
.toLowerCase()
|
|
277
|
+
.replace(/[^a-z0-9]/g, "");
|
|
278
|
+
// Create directory
|
|
279
|
+
if (!fs.existsSync(extensionDir)) {
|
|
280
|
+
fs.mkdirSync(extensionDir, { recursive: true });
|
|
281
|
+
}
|
|
282
|
+
const variables = {
|
|
283
|
+
APP_GROUP_IDENTIFIER: appGroupId,
|
|
284
|
+
APP_URL_SCHEME: urlScheme,
|
|
285
|
+
VERSION: appInfo.version,
|
|
286
|
+
BUNDLE_IDENTIFIER: `${appInfo.identifier}.ShareExtension`,
|
|
287
|
+
PRODUCT_NAME: appInfo.productName,
|
|
288
|
+
};
|
|
289
|
+
// Copy ShareViewController.swift
|
|
290
|
+
const viewControllerSrc = path.join(templatesDir, "ShareViewController.swift");
|
|
291
|
+
if (fs.existsSync(viewControllerSrc)) {
|
|
292
|
+
copyTemplateFile(viewControllerSrc, path.join(extensionDir, "ShareViewController.swift"), variables);
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
throw new Error(`Template not found: ${viewControllerSrc}`);
|
|
296
|
+
}
|
|
297
|
+
// Copy Info.plist
|
|
298
|
+
const possibleInfoPlists = [
|
|
299
|
+
path.join(templatesDir, "Info.plist"),
|
|
300
|
+
path.join(templatesDir, "ShareExtension-Info.plist"),
|
|
301
|
+
];
|
|
302
|
+
let infoPlistFound = false;
|
|
303
|
+
for (const src of possibleInfoPlists) {
|
|
304
|
+
if (fs.existsSync(src)) {
|
|
305
|
+
copyTemplateFile(src, path.join(extensionDir, "Info.plist"), variables);
|
|
306
|
+
infoPlistFound = true;
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (!infoPlistFound) {
|
|
311
|
+
// Create a default Info.plist
|
|
312
|
+
const defaultInfoPlist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
313
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
314
|
+
<plist version="1.0">
|
|
315
|
+
<dict>
|
|
316
|
+
<key>CFBundleDevelopmentRegion</key>
|
|
317
|
+
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
318
|
+
<key>CFBundleDisplayName</key>
|
|
319
|
+
<string>Share</string>
|
|
320
|
+
<key>CFBundleExecutable</key>
|
|
321
|
+
<string>$(EXECUTABLE_NAME)</string>
|
|
322
|
+
<key>CFBundleIdentifier</key>
|
|
323
|
+
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
324
|
+
<key>CFBundleInfoDictionaryVersion</key>
|
|
325
|
+
<string>6.0</string>
|
|
326
|
+
<key>CFBundleName</key>
|
|
327
|
+
<string>$(PRODUCT_NAME)</string>
|
|
328
|
+
<key>CFBundlePackageType</key>
|
|
329
|
+
<string>XPC!</string>
|
|
330
|
+
<key>CFBundleShortVersionString</key>
|
|
331
|
+
<string>${appInfo.version}</string>
|
|
332
|
+
<key>CFBundleVersion</key>
|
|
333
|
+
<string>${appInfo.version}</string>
|
|
334
|
+
<key>NSExtension</key>
|
|
335
|
+
<dict>
|
|
336
|
+
<key>NSExtensionAttributes</key>
|
|
337
|
+
<dict>
|
|
338
|
+
<key>NSExtensionActivationRule</key>
|
|
339
|
+
<dict>
|
|
340
|
+
<key>NSExtensionActivationSupportsFileWithMaxCount</key>
|
|
341
|
+
<integer>10</integer>
|
|
342
|
+
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
|
|
343
|
+
<integer>10</integer>
|
|
344
|
+
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
|
|
345
|
+
<integer>10</integer>
|
|
346
|
+
<key>NSExtensionActivationSupportsText</key>
|
|
347
|
+
<true/>
|
|
348
|
+
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
|
349
|
+
<integer>1</integer>
|
|
350
|
+
</dict>
|
|
351
|
+
</dict>
|
|
352
|
+
<key>NSExtensionPointIdentifier</key>
|
|
353
|
+
<string>com.apple.share-services</string>
|
|
354
|
+
<key>NSExtensionPrincipalClass</key>
|
|
355
|
+
<string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
|
|
356
|
+
</dict>
|
|
357
|
+
</dict>
|
|
358
|
+
</plist>`;
|
|
359
|
+
fs.writeFileSync(path.join(extensionDir, "Info.plist"), defaultInfoPlist);
|
|
360
|
+
}
|
|
361
|
+
// Create entitlements
|
|
362
|
+
createExtensionEntitlements(extensionDir, appGroupId);
|
|
363
|
+
console.log(`Created ShareExtension files in ${extensionDir}`);
|
|
364
|
+
},
|
|
365
|
+
updateProjectYml(projectYml, appInfo) {
|
|
366
|
+
const extensionName = this.extensionName(appInfo);
|
|
367
|
+
const extensionBundleId = `${appInfo.identifier}.ShareExtension`;
|
|
368
|
+
const targetName = `${appInfo.productName}_iOS`;
|
|
369
|
+
// Create the extension target YAML
|
|
370
|
+
const extensionTarget = `
|
|
371
|
+
${extensionName}:
|
|
372
|
+
type: app-extension
|
|
373
|
+
platform: iOS
|
|
374
|
+
deploymentTarget: "14.0"
|
|
375
|
+
sources:
|
|
376
|
+
- path: ShareExtension
|
|
377
|
+
info:
|
|
378
|
+
path: ShareExtension/Info.plist
|
|
379
|
+
properties:
|
|
380
|
+
CFBundleDisplayName: Share
|
|
381
|
+
CFBundleShortVersionString: "${appInfo.version}"
|
|
382
|
+
CFBundleVersion: "${appInfo.version}"
|
|
383
|
+
NSExtension:
|
|
384
|
+
NSExtensionAttributes:
|
|
385
|
+
NSExtensionActivationRule:
|
|
386
|
+
NSExtensionActivationSupportsFileWithMaxCount: 10
|
|
387
|
+
NSExtensionActivationSupportsImageWithMaxCount: 10
|
|
388
|
+
NSExtensionActivationSupportsMovieWithMaxCount: 10
|
|
389
|
+
NSExtensionActivationSupportsText: true
|
|
390
|
+
NSExtensionActivationSupportsWebURLWithMaxCount: 1
|
|
391
|
+
NSExtensionPointIdentifier: com.apple.share-services
|
|
392
|
+
NSExtensionPrincipalClass: $(PRODUCT_MODULE_NAME).ShareViewController
|
|
393
|
+
settings:
|
|
394
|
+
base:
|
|
395
|
+
PRODUCT_BUNDLE_IDENTIFIER: ${extensionBundleId}
|
|
396
|
+
SKIP_INSTALL: YES
|
|
397
|
+
CODE_SIGN_ENTITLEMENTS: ShareExtension/ShareExtension.entitlements
|
|
398
|
+
`;
|
|
399
|
+
// Add the target
|
|
400
|
+
let modified = addExtensionTarget(projectYml, {
|
|
401
|
+
name: extensionName,
|
|
402
|
+
yaml: extensionTarget,
|
|
403
|
+
});
|
|
404
|
+
// Add dependency to main target
|
|
405
|
+
modified = addDependencyToTarget(modified, targetName, {
|
|
406
|
+
target: extensionName,
|
|
407
|
+
});
|
|
408
|
+
return modified;
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const __filename$1 = fileURLToPath(import.meta.url);
|
|
413
|
+
const __dirname$1 = path.dirname(__filename$1);
|
|
414
|
+
const EXTENSIONS = {
|
|
415
|
+
share: shareExtension,
|
|
416
|
+
};
|
|
417
|
+
function resolveTemplatesDir(options, extensionType) {
|
|
418
|
+
// Option 1: Explicit templates path
|
|
419
|
+
if (options.templates) {
|
|
420
|
+
const templatesPath = path.resolve(process.cwd(), options.templates);
|
|
421
|
+
if (!fs.existsSync(templatesPath)) {
|
|
422
|
+
throw new Error(`Templates directory not found: ${templatesPath}`);
|
|
423
|
+
}
|
|
424
|
+
return templatesPath;
|
|
425
|
+
}
|
|
426
|
+
// Option 2: Plugin templates
|
|
427
|
+
if (options.plugin) {
|
|
428
|
+
const pluginPath = resolvePluginPath(options.plugin);
|
|
429
|
+
const pluginPkg = JSON.parse(fs.readFileSync(path.join(pluginPath, "package.json"), "utf8"));
|
|
430
|
+
const extensionConfig = pluginPkg["tauri-apple-extension"];
|
|
431
|
+
if (!extensionConfig) {
|
|
432
|
+
throw new Error(`Plugin ${options.plugin} does not have tauri-apple-extension config in package.json`);
|
|
433
|
+
}
|
|
434
|
+
if (extensionConfig.type !== extensionType) {
|
|
435
|
+
throw new Error(`Plugin ${options.plugin} is for '${extensionConfig.type}' extension, not '${extensionType}'`);
|
|
436
|
+
}
|
|
437
|
+
const templatesPath = path.join(pluginPath, extensionConfig.templates);
|
|
438
|
+
if (!fs.existsSync(templatesPath)) {
|
|
439
|
+
throw new Error(`Plugin templates directory not found: ${templatesPath}`);
|
|
440
|
+
}
|
|
441
|
+
return templatesPath;
|
|
442
|
+
}
|
|
443
|
+
// Option 3: Default templates
|
|
444
|
+
const defaultTemplates = path.join(__dirname$1, "../../templates", extensionType);
|
|
445
|
+
if (!fs.existsSync(defaultTemplates)) {
|
|
446
|
+
throw new Error(`No templates found. Use --plugin or --templates to specify templates.`);
|
|
447
|
+
}
|
|
448
|
+
return defaultTemplates;
|
|
449
|
+
}
|
|
450
|
+
function resolvePluginPath(pluginName) {
|
|
451
|
+
// Try to find in node_modules
|
|
452
|
+
const possiblePaths = [
|
|
453
|
+
path.join(process.cwd(), "node_modules", pluginName),
|
|
454
|
+
path.join(process.cwd(), "src-tauri", "node_modules", pluginName),
|
|
455
|
+
];
|
|
456
|
+
for (const p of possiblePaths) {
|
|
457
|
+
if (fs.existsSync(p)) {
|
|
458
|
+
return p;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
throw new Error(`Plugin ${pluginName} not found in node_modules. Make sure it's installed.`);
|
|
462
|
+
}
|
|
463
|
+
async function addExtension(type, options) {
|
|
464
|
+
console.log(`\nTauri Apple Extensions - Add ${type}\n`);
|
|
465
|
+
try {
|
|
466
|
+
// Validate extension type
|
|
467
|
+
const extension = EXTENSIONS[type];
|
|
468
|
+
if (!extension) {
|
|
469
|
+
const available = Object.keys(EXTENSIONS).join(", ");
|
|
470
|
+
throw new Error(`Unknown extension type: ${type}. Available: ${available}`);
|
|
471
|
+
}
|
|
472
|
+
// Find project
|
|
473
|
+
const projectRoot = findProjectRoot();
|
|
474
|
+
console.log(`Project root: ${projectRoot}`);
|
|
475
|
+
const tauriConfig = findTauriConfig(projectRoot);
|
|
476
|
+
const appleDir = findAppleProjectDir(projectRoot);
|
|
477
|
+
console.log(`Apple project dir: ${appleDir}`);
|
|
478
|
+
// Get app info
|
|
479
|
+
let projectYml = readProjectYml(appleDir);
|
|
480
|
+
const appInfo = getAppInfo(tauriConfig, projectYml);
|
|
481
|
+
console.log(`\nApp Info:`);
|
|
482
|
+
console.log(` Product Name: ${appInfo.productName}`);
|
|
483
|
+
console.log(` Bundle ID: ${appInfo.identifier}`);
|
|
484
|
+
console.log(` Version: ${appInfo.version}`);
|
|
485
|
+
const appGroupId = `group.${appInfo.identifier}`;
|
|
486
|
+
const urlScheme = appInfo.productName
|
|
487
|
+
.toLowerCase()
|
|
488
|
+
.replace(/[^a-z0-9]/g, "");
|
|
489
|
+
console.log(` App Group: ${appGroupId}`);
|
|
490
|
+
console.log(` URL Scheme: ${urlScheme}`);
|
|
491
|
+
// Resolve templates
|
|
492
|
+
const templatesDir = resolveTemplatesDir(options, type);
|
|
493
|
+
console.log(`\nUsing templates from: ${templatesDir}`);
|
|
494
|
+
// Run extension setup
|
|
495
|
+
console.log(`\n1. Creating ${extension.displayName} files...`);
|
|
496
|
+
extension.createFiles(appleDir, appInfo, templatesDir);
|
|
497
|
+
console.log(`\n2. Updating main app entitlements...`);
|
|
498
|
+
updateMainAppEntitlements(appleDir, appInfo);
|
|
499
|
+
console.log(`\n3. Adding URL scheme to Info.plist...`);
|
|
500
|
+
addUrlSchemeToInfoPlist(appleDir, appInfo);
|
|
501
|
+
console.log(`\n4. Updating project.yml...`);
|
|
502
|
+
projectYml = readProjectYml(appleDir);
|
|
503
|
+
projectYml = extension.updateProjectYml(projectYml, appInfo);
|
|
504
|
+
writeProjectYml(appleDir, projectYml);
|
|
505
|
+
console.log(`\n5. Regenerating Xcode project...`);
|
|
506
|
+
runXcodeGen(appleDir);
|
|
507
|
+
console.log(`\n========================================`);
|
|
508
|
+
console.log(`${extension.displayName} setup complete!`);
|
|
509
|
+
console.log(`========================================\n`);
|
|
510
|
+
console.log(`Next steps:`);
|
|
511
|
+
console.log(`1. Open the Xcode project`);
|
|
512
|
+
console.log(`2. Select your team for both targets (main app and ${extension.extensionName(appInfo)})`);
|
|
513
|
+
console.log(`3. Enable 'App Groups' capability for both targets with: ${appGroupId}`);
|
|
514
|
+
console.log(`4. Build and run!\n`);
|
|
515
|
+
console.log(`IMPORTANT: You need to configure App Groups in Apple Developer Portal:`);
|
|
516
|
+
console.log(` - Create App Group: ${appGroupId}`);
|
|
517
|
+
console.log(` - Add it to both App IDs: ${appInfo.identifier} and ${appInfo.identifier}.${extension.extensionSuffix}\n`);
|
|
518
|
+
}
|
|
519
|
+
catch (error) {
|
|
520
|
+
console.error(`\nError: ${error.message}`);
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const program = new Command();
|
|
526
|
+
program
|
|
527
|
+
.name("tauri-apple-extensions")
|
|
528
|
+
.description("Add iOS extensions to Tauri apps")
|
|
529
|
+
.version("0.1.0");
|
|
530
|
+
program
|
|
531
|
+
.command("add <type>")
|
|
532
|
+
.description("Add an extension (e.g., share)")
|
|
533
|
+
.option("-p, --plugin <name>", "Plugin to use for templates")
|
|
534
|
+
.option("-t, --templates <path>", "Custom templates directory")
|
|
535
|
+
.action(addExtension);
|
|
536
|
+
program.parse();
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AppInfo, TauriConfig } from "../types.js";
|
|
2
|
+
export declare function findProjectRoot(): string;
|
|
3
|
+
export declare function findTauriConfig(projectRoot: string): TauriConfig;
|
|
4
|
+
export declare function findAppleProjectDir(projectRoot: string): string;
|
|
5
|
+
export declare function getAppInfo(tauriConfig: TauriConfig, projectYml: string): AppInfo;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { TargetConfig, DependencyConfig } from "../types.js";
|
|
2
|
+
export declare function readProjectYml(appleDir: string): string;
|
|
3
|
+
export declare function writeProjectYml(appleDir: string, content: string): void;
|
|
4
|
+
export declare function addExtensionTarget(projectYml: string, targetConfig: TargetConfig): string;
|
|
5
|
+
export declare function addDependencyToTarget(projectYml: string, mainTargetName: string, dependencyConfig: DependencyConfig): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runXcodeGen(appleDir: string): void;
|