@stacksjs/browser-extension 0.70.140 → 0.70.142
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/README.md +25 -9
- package/dist/app-store-connect.d.ts +20 -1
- package/dist/app-store-connect.js +60 -2
- package/dist/safari.d.ts +40 -2
- package/dist/safari.js +180 -30
- package/dist/types.d.ts +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,8 +4,9 @@ Build MV3 browser extensions (Chrome + Firefox + Safari) the Stacks way — the
|
|
|
4
4
|
manifest, content/background scripts, stx pages, `declarativeNetRequest`
|
|
5
5
|
rulesets, and store-ready packaging are all derived from a single
|
|
6
6
|
`config/extension.ts`. No hand-written `manifest.json`, no per-project build
|
|
7
|
-
script. Safari additionally gets
|
|
8
|
-
|
|
7
|
+
script. Safari additionally gets macOS, iPhone, and iPad container apps from
|
|
8
|
+
one web bundle, with checked-in macOS development and universal App Store
|
|
9
|
+
packaging owned by the framework.
|
|
9
10
|
|
|
10
11
|
## Quick start
|
|
11
12
|
|
|
@@ -46,7 +47,7 @@ repository instead of duplicating store orchestration:
|
|
|
46
47
|
```yaml
|
|
47
48
|
jobs:
|
|
48
49
|
publish:
|
|
49
|
-
uses: stacksjs/stacks/.github/workflows/browser-extension-release.yml@v0.70.
|
|
50
|
+
uses: stacksjs/stacks/.github/workflows/browser-extension-release.yml@v0.70.142
|
|
50
51
|
with:
|
|
51
52
|
chrome-publisher-id: ${{ vars.CHROME_WEB_STORE_PUBLISHER_ID }}
|
|
52
53
|
safari-enabled: ${{ vars.ENABLE_SAFARI_PUBLISH == 'true' }}
|
|
@@ -65,9 +66,9 @@ GitHub Release only after the enabled stores succeed.
|
|
|
65
66
|
|
|
66
67
|
## Safari
|
|
67
68
|
|
|
68
|
-
Safari Web Extensions ship inside
|
|
69
|
-
halves: the web bundle (`extension:build --target safari`) and the
|
|
70
|
-
|
|
69
|
+
Safari Web Extensions ship inside Apple container apps, so the safari target
|
|
70
|
+
has two halves: the web bundle (`extension:build --target safari`) and the
|
|
71
|
+
macOS/iOS apps. The build rewrites promise-style `chrome.*` to `browser.*` (Safari's
|
|
71
72
|
`chrome.*` is callback-flavoured) and pins
|
|
72
73
|
`browser_specific_settings.safari.strict_min_version` (default 18.4, the first
|
|
73
74
|
Safari with MAIN-world content scripts + `match_about_blank`).
|
|
@@ -75,12 +76,17 @@ Safari with MAIN-world content scripts + `match_about_blank`).
|
|
|
75
76
|
```sh
|
|
76
77
|
buddy extension:safari:init # scaffold the Xcode container app into safari/
|
|
77
78
|
buddy extension:safari:provision # register both Bundle IDs + check the app record
|
|
78
|
-
buddy extension:safari:app # build
|
|
79
|
-
buddy extension:safari:
|
|
79
|
+
buddy extension:safari:app # build configured macOS/iOS apps
|
|
80
|
+
buddy extension:safari:app --platform ios # build for iPhone/iPad Simulator
|
|
81
|
+
buddy extension:safari:publish # archive + upload every configured Apple platform
|
|
80
82
|
```
|
|
81
83
|
|
|
82
84
|
Set `safariBundleId` in the config (the appex gets `<safariBundleId>.Extension`)
|
|
83
|
-
and `safariTeamId` to the Apple Developer team used for signing.
|
|
85
|
+
and `safariTeamId` to the Apple Developer team used for signing. Set
|
|
86
|
+
`safariPlatforms: ['macos', 'ios']` for a universal extension; iOS covers both
|
|
87
|
+
iPhone and iPad. Stacks generates the current Apple-supported universal Xcode
|
|
88
|
+
project from the same built extension, archives both platforms, and uploads
|
|
89
|
+
both to the same App Store Connect app record. Publishing
|
|
84
90
|
reads `APP_STORE_CONNECT_API_KEY_ID`, `APP_STORE_CONNECT_API_ISSUER_ID`, and
|
|
85
91
|
`APP_STORE_CONNECT_API_KEY_PATH` from the environment. Run with
|
|
86
92
|
`--validate-only` to exercise Apple's validation without uploading a build.
|
|
@@ -90,6 +96,11 @@ what `xcrun safari-web-extension-converter` generates, so day-to-day work
|
|
|
90
96
|
never needs the converter; `--signed` builds need an Apple Development
|
|
91
97
|
identity selected in Xcode.
|
|
92
98
|
|
|
99
|
+
For device testing, install the generated iOS app from Xcode, then enable the
|
|
100
|
+
extension in Settings > Apps > Safari > Extensions. The iOS target supports
|
|
101
|
+
iPhone and iPad from iOS 15 onward; the extension's configured Safari minimum
|
|
102
|
+
version can require a newer OS.
|
|
103
|
+
|
|
93
104
|
## Configure
|
|
94
105
|
|
|
95
106
|
```ts
|
|
@@ -103,6 +114,9 @@ export default defineExtension({
|
|
|
103
114
|
chromeWebStore: { publisherId: 'publisher-id', itemId: 'extension-id' },
|
|
104
115
|
firefoxAddons: { license: 'MIT', categories: ['privacy-security'] },
|
|
105
116
|
safariBundleId: 'com.example.MyExtension', // Safari container app bundle id
|
|
117
|
+
safariTeamId: 'TEAM123456',
|
|
118
|
+
safariPlatforms: ['macos', 'ios'], // iOS includes iPhone and iPad
|
|
119
|
+
safariAppCategory: 'public.app-category.utilities',
|
|
106
120
|
targets: ['chrome', 'firefox'],
|
|
107
121
|
|
|
108
122
|
background: 'src/background/index.ts',
|
|
@@ -164,5 +178,7 @@ import {
|
|
|
164
178
|
scaffoldSafariApp,
|
|
165
179
|
syncSafariResources,
|
|
166
180
|
buildSafariApp,
|
|
181
|
+
buildSafariUniversalApp,
|
|
182
|
+
createSafariUniversalProject,
|
|
167
183
|
} from '@stacksjs/browser-extension'
|
|
168
184
|
```
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AppStoreConnectAuth } from './safari';
|
|
2
|
-
import type { ExtensionConfig } from './types';
|
|
2
|
+
import type { ExtensionConfig, SafariPlatform } from './types';
|
|
3
3
|
/** Generate the short-lived ES256 team token required by App Store Connect. */
|
|
4
4
|
export declare function appStoreConnectToken(auth: Required<AppStoreConnectAuth>, now?: unknown): string;
|
|
5
5
|
/**
|
|
@@ -25,21 +25,37 @@ export declare interface AppAttributes extends Record<string, unknown> {
|
|
|
25
25
|
primaryLocale: string
|
|
26
26
|
sku: string
|
|
27
27
|
}
|
|
28
|
+
export declare interface AppStoreVersionAttributes extends Record<string, unknown> {
|
|
29
|
+
platform: AppStoreVersionPlatform
|
|
30
|
+
versionString: string
|
|
31
|
+
appStoreState: string
|
|
32
|
+
}
|
|
28
33
|
export declare interface AppStoreConnectClientOptions extends AppStoreConnectAuth {
|
|
29
34
|
baseUrl?: string
|
|
30
35
|
fetch?: typeof globalThis.fetch
|
|
31
36
|
now?: () => number
|
|
32
37
|
}
|
|
38
|
+
export declare interface SafariAppStoreVersionResult {
|
|
39
|
+
platform: SafariPlatform
|
|
40
|
+
version: string
|
|
41
|
+
created: boolean
|
|
42
|
+
updated: boolean
|
|
43
|
+
id: string
|
|
44
|
+
}
|
|
33
45
|
export declare interface SafariProvisionOptions extends AppStoreConnectClientOptions {
|
|
34
46
|
checkOnly?: boolean
|
|
35
47
|
platform?: BundleIdPlatform
|
|
48
|
+
version?: string
|
|
49
|
+
platforms?: SafariPlatform[]
|
|
36
50
|
}
|
|
37
51
|
export declare interface SafariProvisionResult {
|
|
38
52
|
container: { identifier: string, exists: boolean, created: boolean }
|
|
39
53
|
extension: { identifier: string, exists: boolean, created: boolean }
|
|
40
54
|
appRecord: { exists: boolean, id?: string }
|
|
55
|
+
appStoreVersions: SafariAppStoreVersionResult[]
|
|
41
56
|
}
|
|
42
57
|
export type BundleIdPlatform = 'IOS' | 'MAC_OS' | 'UNIVERSAL';
|
|
58
|
+
export type AppStoreVersionPlatform = 'IOS' | 'MAC_OS';
|
|
43
59
|
/** Minimal official App Store Connect client for Safari provisioning checks. */
|
|
44
60
|
export declare class AppStoreConnectClient {
|
|
45
61
|
constructor(options?: AppStoreConnectClientOptions);
|
|
@@ -47,4 +63,7 @@ export declare class AppStoreConnectClient {
|
|
|
47
63
|
registerBundleId(identifier: string, name: string, platform?: BundleIdPlatform): Promise<AppStoreConnectResource<BundleIdAttributes>>;
|
|
48
64
|
ensureBundleId(identifier: string, name: string, options?: { checkOnly?: boolean, platform?: BundleIdPlatform }): Promise<{ bundleId?: AppStoreConnectResource<BundleIdAttributes>, created: boolean }>;
|
|
49
65
|
findApp(bundleId: string): Promise<AppStoreConnectResource<AppAttributes> | undefined>;
|
|
66
|
+
listAppStoreVersions(appId: string): Promise<Array<AppStoreConnectResource<AppStoreVersionAttributes>>>;
|
|
67
|
+
createAppStoreVersion(appId: string, platform: AppStoreVersionPlatform, versionString: string): Promise<AppStoreConnectResource<AppStoreVersionAttributes>>;
|
|
68
|
+
updateAppStoreVersion(versionId: string, versionString: string): Promise<AppStoreConnectResource<AppStoreVersionAttributes>>;
|
|
50
69
|
}
|
|
@@ -79,14 +79,72 @@ export class AppStoreConnectClient {
|
|
|
79
79
|
const query = new URLSearchParams({ "filter[bundleId]": bundleId });
|
|
80
80
|
return (await this.request(`/apps?${query}`)).data.find((app) => app.attributes.bundleId === bundleId);
|
|
81
81
|
}
|
|
82
|
+
async listAppStoreVersions(appId) {
|
|
83
|
+
return (await this.request(`/apps/${appId}/appStoreVersions?limit=200`)).data;
|
|
84
|
+
}
|
|
85
|
+
async createAppStoreVersion(appId, platform, versionString) {
|
|
86
|
+
return (await this.request("/appStoreVersions", {
|
|
87
|
+
method: "POST",
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
data: {
|
|
90
|
+
type: "appStoreVersions",
|
|
91
|
+
attributes: { platform, versionString },
|
|
92
|
+
relationships: { app: { data: { type: "apps", id: appId } } }
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
})).data;
|
|
96
|
+
}
|
|
97
|
+
async updateAppStoreVersion(versionId, versionString) {
|
|
98
|
+
return (await this.request(`/appStoreVersions/${versionId}`, {
|
|
99
|
+
method: "PATCH",
|
|
100
|
+
body: JSON.stringify({
|
|
101
|
+
data: {
|
|
102
|
+
type: "appStoreVersions",
|
|
103
|
+
id: versionId,
|
|
104
|
+
attributes: { versionString }
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
})).data;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const editableVersionStates = new Set([
|
|
111
|
+
"PREPARE_FOR_SUBMISSION",
|
|
112
|
+
"DEVELOPER_REJECTED",
|
|
113
|
+
"REJECTED",
|
|
114
|
+
"METADATA_REJECTED"
|
|
115
|
+
]);
|
|
116
|
+
function appStorePlatform(platform) {
|
|
117
|
+
return platform === "ios" ? "IOS" : "MAC_OS";
|
|
118
|
+
}
|
|
119
|
+
async function ensureConfiguredAppStoreVersions(client, appId, platforms, version) {
|
|
120
|
+
const versions = await client.listAppStoreVersions(appId), results = [];
|
|
121
|
+
for (const platform of [...new Set(platforms)]) {
|
|
122
|
+
const applePlatform = appStorePlatform(platform), exact = versions.find((item) => item.attributes.platform === applePlatform && item.attributes.versionString === version);
|
|
123
|
+
if (exact) {
|
|
124
|
+
results.push({ platform, version, created: !1, updated: !1, id: exact.id });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const editable = versions.find((item) => item.attributes.platform === applePlatform && editableVersionStates.has(item.attributes.appStoreState));
|
|
128
|
+
if (editable) {
|
|
129
|
+
const updated = await client.updateAppStoreVersion(editable.id, version);
|
|
130
|
+
editable.attributes.versionString = version;
|
|
131
|
+
results.push({ platform, version, created: !1, updated: !0, id: updated.id });
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const created = await client.createAppStoreVersion(appId, applePlatform, version);
|
|
135
|
+
versions.push(created);
|
|
136
|
+
results.push({ platform, version, created: !0, updated: !1, id: created.id });
|
|
137
|
+
}
|
|
138
|
+
return results;
|
|
82
139
|
}
|
|
83
140
|
export async function provisionSafariApp(config, options = {}) {
|
|
84
141
|
if (!config.safariBundleId)
|
|
85
142
|
throw Error("[browser-extension] Safari provisioning needs safariBundleId in config/extension.ts");
|
|
86
|
-
const client = new AppStoreConnectClient(options), identifier = config.safariBundleId, extensionIdentifier = `${identifier}.Extension`, container = await client.ensureBundleId(identifier, config.name,
|
|
143
|
+
const client = new AppStoreConnectClient(options), identifier = config.safariBundleId, extensionIdentifier = `${identifier}.Extension`, platforms = options.platforms ?? config.safariPlatforms ?? ["macos"], bundleIdPlatform = options.platform ?? (platforms.includes("ios") ? "UNIVERSAL" : "MAC_OS"), bundleOptions = { checkOnly: options.checkOnly, platform: bundleIdPlatform }, container = await client.ensureBundleId(identifier, config.name, bundleOptions), extension = await client.ensureBundleId(extensionIdentifier, `${config.name} Safari Extension`, bundleOptions), app = await client.findApp(identifier), appStoreVersions = app && options.version && !options.checkOnly ? await ensureConfiguredAppStoreVersions(client, app.id, platforms, options.version) : [];
|
|
87
144
|
return {
|
|
88
145
|
container: { identifier, exists: Boolean(container.bundleId), created: container.created },
|
|
89
146
|
extension: { identifier: extensionIdentifier, exists: Boolean(extension.bundleId), created: extension.created },
|
|
90
|
-
appRecord: { exists: Boolean(app), id: app?.id }
|
|
147
|
+
appRecord: { exists: Boolean(app), id: app?.id },
|
|
148
|
+
appStoreVersions
|
|
91
149
|
};
|
|
92
150
|
}
|
package/dist/safari.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionConfig } from './types';
|
|
1
|
+
import type { ExtensionConfig, SafariPlatform } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* Safari container-app tooling: scaffold the Xcode project, sync the built
|
|
4
4
|
* safari bundle into the appex Resources, and xcodebuild the app.
|
|
@@ -18,6 +18,9 @@ import type { ExtensionConfig } from './types';
|
|
|
18
18
|
export declare function safariAppName(config: ExtensionConfig): string;
|
|
19
19
|
/** Directory the container app is scaffolded into. */
|
|
20
20
|
export declare function safariProjectDir(cwd?: unknown, dir?: string): string;
|
|
21
|
+
/** Safari platforms selected in config, de-duplicated in stable order. */
|
|
22
|
+
export declare function resolveSafariPlatforms(config: ExtensionConfig, platforms?: SafariPlatform[]): SafariPlatform[];
|
|
23
|
+
export declare function safariPackagerArgs(input: string, projectLocation: string, appName: string, bundleId: string, platforms: SafariPlatform[]): string[];
|
|
21
24
|
/** Upgrade a Stacks-generated project that copied Resources as a nested folder. */
|
|
22
25
|
export declare function migrateSafariResourceBuildPhase(project: string, appName: string): string;
|
|
23
26
|
/**
|
|
@@ -34,6 +37,10 @@ export declare function scaffoldSafariApp(config: ExtensionConfig, options?: Saf
|
|
|
34
37
|
* marketing-site pages built into dist) are kept out.
|
|
35
38
|
*/
|
|
36
39
|
export declare function syncSafariResources(config: ExtensionConfig, options?: SafariSyncOptions): Promise<{ resources: string, files: number }>;
|
|
40
|
+
/** Generate a clean Apple-supported Safari project for macOS, iOS, or both. */
|
|
41
|
+
export declare function createSafariUniversalProject(config: ExtensionConfig, options?: SafariUniversalProjectOptions): Promise<SafariUniversalProject>;
|
|
42
|
+
/** Build Apple-supported macOS and iOS Safari containers from one web bundle. */
|
|
43
|
+
export declare function buildSafariUniversalApp(config: ExtensionConfig, options?: SafariUniversalBuildOptions): Promise<SafariUniversalBuildResult>;
|
|
37
44
|
/**
|
|
38
45
|
* Full pipeline: build the safari bundle, sync it into the appex Resources,
|
|
39
46
|
* then xcodebuild the container app when full Xcode is available. Returns the
|
|
@@ -47,7 +54,7 @@ export declare function resolveAppStoreConnectAuth(options: AppStoreConnectAuth)
|
|
|
47
54
|
* Store Connect. Xcode owns certificate/profile creation and the upload so the
|
|
48
55
|
* same command works locally and in macOS CI with an API key.
|
|
49
56
|
*/
|
|
50
|
-
export declare function publishSafariApp(config: ExtensionConfig, options: SafariPublishOptions): Promise<
|
|
57
|
+
export declare function publishSafariApp(config: ExtensionConfig, options: SafariPublishOptions): Promise<SafariPublishResult>;
|
|
51
58
|
export declare interface SafariScaffoldOptions {
|
|
52
59
|
bundleId?: string
|
|
53
60
|
dir?: string
|
|
@@ -62,6 +69,25 @@ export declare interface SafariSyncOptions {
|
|
|
62
69
|
dir?: string
|
|
63
70
|
cwd?: string
|
|
64
71
|
}
|
|
72
|
+
export declare interface SafariUniversalProjectOptions extends SafariSyncOptions {
|
|
73
|
+
platforms?: SafariPlatform[]
|
|
74
|
+
build?: boolean
|
|
75
|
+
version?: string
|
|
76
|
+
}
|
|
77
|
+
export declare interface SafariUniversalProject {
|
|
78
|
+
dir: string
|
|
79
|
+
project: string
|
|
80
|
+
resources: string
|
|
81
|
+
platforms: SafariPlatform[]
|
|
82
|
+
}
|
|
83
|
+
export declare interface SafariUniversalBuildOptions extends SafariUniversalProjectOptions {
|
|
84
|
+
release?: boolean
|
|
85
|
+
signed?: boolean
|
|
86
|
+
skipXcodebuild?: boolean
|
|
87
|
+
}
|
|
88
|
+
export declare interface SafariUniversalBuildResult extends SafariUniversalProject {
|
|
89
|
+
appPaths: Partial<Record<SafariPlatform, string>>
|
|
90
|
+
}
|
|
65
91
|
export declare interface SafariAppBuildOptions extends SafariSyncOptions {
|
|
66
92
|
release?: boolean
|
|
67
93
|
signed?: boolean
|
|
@@ -80,4 +106,16 @@ export declare interface SafariPublishOptions extends SafariSyncOptions, AppStor
|
|
|
80
106
|
teamId?: string
|
|
81
107
|
validateOnly?: boolean
|
|
82
108
|
build?: boolean
|
|
109
|
+
platforms?: SafariPlatform[]
|
|
110
|
+
}
|
|
111
|
+
export declare interface SafariPublishedArtifact {
|
|
112
|
+
platform: SafariPlatform
|
|
113
|
+
archivePath: string
|
|
114
|
+
exportPath: string
|
|
115
|
+
}
|
|
116
|
+
export declare interface SafariPublishResult {
|
|
117
|
+
archivePath: string
|
|
118
|
+
exportPath: string
|
|
119
|
+
buildNumber: string
|
|
120
|
+
artifacts: SafariPublishedArtifact[]
|
|
83
121
|
}
|
package/dist/safari.js
CHANGED
|
@@ -11,6 +11,34 @@ export function safariAppName(config) {
|
|
|
11
11
|
export function safariProjectDir(cwd = process.cwd(), dir = "safari") {
|
|
12
12
|
return resolve(cwd, dir);
|
|
13
13
|
}
|
|
14
|
+
export function resolveSafariPlatforms(config, platforms) {
|
|
15
|
+
const selected = platforms ?? config.safariPlatforms ?? ["macos"];
|
|
16
|
+
return [...new Set(selected)];
|
|
17
|
+
}
|
|
18
|
+
function safariPlatformFlag(platforms) {
|
|
19
|
+
if (platforms.length !== 1)
|
|
20
|
+
return;
|
|
21
|
+
return platforms[0] === "ios" ? "--ios-only" : "--macos-only";
|
|
22
|
+
}
|
|
23
|
+
export function safariPackagerArgs(input, projectLocation, appName, bundleId, platforms) {
|
|
24
|
+
const platformFlag = safariPlatformFlag(platforms);
|
|
25
|
+
return [
|
|
26
|
+
"safari-web-extension-packager",
|
|
27
|
+
"--project-location",
|
|
28
|
+
projectLocation,
|
|
29
|
+
"--app-name",
|
|
30
|
+
appName,
|
|
31
|
+
"--bundle-identifier",
|
|
32
|
+
bundleId,
|
|
33
|
+
"--swift",
|
|
34
|
+
"--copy-resources",
|
|
35
|
+
"--no-open",
|
|
36
|
+
"--no-prompt",
|
|
37
|
+
"--force",
|
|
38
|
+
...platformFlag ? [platformFlag] : [],
|
|
39
|
+
input
|
|
40
|
+
];
|
|
41
|
+
}
|
|
14
42
|
function templateRoot() {
|
|
15
43
|
return resolve(import.meta.dir, "..", "safari-template");
|
|
16
44
|
}
|
|
@@ -41,6 +69,8 @@ function safariResourceCopyBuildPhase(appName) {
|
|
|
41
69
|
`;
|
|
42
70
|
}
|
|
43
71
|
export function migrateSafariResourceBuildPhase(project, appName) {
|
|
72
|
+
if (project.includes("resources_prefix='$(SRCROOT)"))
|
|
73
|
+
return project.replace(/\/\* Begin PBXShellScriptBuildPhase section \*\/[\s\S]*?\/\* End PBXShellScriptBuildPhase section \*\/\n/, safariResourceCopyBuildPhase(appName));
|
|
44
74
|
if (project.includes("SCRIPT_INPUT_FILE_LIST_0"))
|
|
45
75
|
return project;
|
|
46
76
|
if (project.includes("0D0000000000000000000023 /* Copy Web Extension Resources */"))
|
|
@@ -123,24 +153,12 @@ async function generateAppIcons(config, dir, vars, iconsDir, cwd) {
|
|
|
123
153
|
return;
|
|
124
154
|
}
|
|
125
155
|
}
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
throw Error(`[browser-extension] ${outdir}/manifest.json is missing. Run extension:build --target safari first.`);
|
|
130
|
-
const appName = safariAppName(config), projectDir = safariProjectDir(cwd, options.dir), projectPath = join(projectDir, `${appName}.xcodeproj`, "project.pbxproj");
|
|
131
|
-
if (existsSync(projectPath)) {
|
|
132
|
-
const project = await Bun.file(projectPath).text(), migrated = migrateSafariResourceBuildPhase(project, appName);
|
|
133
|
-
if (migrated !== project)
|
|
134
|
-
await Bun.write(projectPath, migrated);
|
|
135
|
-
}
|
|
136
|
-
const resources = join(projectDir, `${appName} Extension`, "Resources"), exclude = new Set(config.safariExclude ?? []);
|
|
137
|
-
if (existsSync(resources)) {
|
|
138
|
-
for (const entry of readdirSync(resources))
|
|
139
|
-
if (entry !== ".gitkeep")
|
|
140
|
-
await rm(join(resources, entry), { recursive: !0, force: !0 });
|
|
141
|
-
}
|
|
156
|
+
async function mirrorSafariResources(config, outdir, resources, keepPlaceholder = !1) {
|
|
157
|
+
const exclude = new Set(config.safariExclude ?? []);
|
|
158
|
+
await rm(resources, { recursive: !0, force: !0 });
|
|
142
159
|
await mkdir(resources, { recursive: !0 });
|
|
143
|
-
|
|
160
|
+
if (keepPlaceholder)
|
|
161
|
+
await Bun.write(join(resources, ".gitkeep"), "");
|
|
144
162
|
const synced = [];
|
|
145
163
|
for (const rel of walk(outdir)) {
|
|
146
164
|
if (exclude.has(rel) || rel.split("/").includes(".DS_Store"))
|
|
@@ -149,16 +167,84 @@ export async function syncSafariResources(config, options = {}) {
|
|
|
149
167
|
await mkdir(dirname(dest), { recursive: !0 });
|
|
150
168
|
cpSync(join(outdir, rel), dest);
|
|
151
169
|
synced.push(rel);
|
|
152
|
-
files += 1;
|
|
153
170
|
}
|
|
154
|
-
|
|
171
|
+
return synced;
|
|
172
|
+
}
|
|
173
|
+
export async function syncSafariResources(config, options = {}) {
|
|
174
|
+
const cwd = options.cwd ?? process.cwd(), outdir = resolve(cwd, options.outdir ?? resolveOutdir(config, "safari"));
|
|
175
|
+
if (!existsSync(join(outdir, "manifest.json")))
|
|
176
|
+
throw Error(`[browser-extension] ${outdir}/manifest.json is missing. Run extension:build --target safari first.`);
|
|
177
|
+
const appName = safariAppName(config), projectDir = safariProjectDir(cwd, options.dir), projectPath = join(projectDir, `${appName}.xcodeproj`, "project.pbxproj");
|
|
178
|
+
if (existsSync(projectPath)) {
|
|
179
|
+
const project = await Bun.file(projectPath).text(), migrated = migrateSafariResourceBuildPhase(project, appName);
|
|
180
|
+
if (migrated !== project)
|
|
181
|
+
await Bun.write(projectPath, migrated);
|
|
182
|
+
}
|
|
183
|
+
const resources = join(projectDir, `${appName} Extension`, "Resources"), synced = await mirrorSafariResources(config, outdir, resources, !0), inputs = synced.map((rel) => `$(SRCROOT)/${appName} Extension/Resources/${rel}`).join(`
|
|
155
184
|
`), outputs = synced.map((rel) => `$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/${rel}`).join(`
|
|
156
185
|
`);
|
|
157
186
|
await Bun.write(join(projectDir, `${appName} Extension`, "Resources.inputs.xcfilelist"), `${inputs}
|
|
158
187
|
`);
|
|
159
188
|
await Bun.write(join(projectDir, `${appName} Extension`, "Resources.outputs.xcfilelist"), `${outputs}
|
|
160
189
|
`);
|
|
161
|
-
return { resources, files };
|
|
190
|
+
return { resources, files: synced.length };
|
|
191
|
+
}
|
|
192
|
+
export async function createSafariUniversalProject(config, options = {}) {
|
|
193
|
+
const cwd = options.cwd ?? process.cwd();
|
|
194
|
+
if (options.build !== !1) {
|
|
195
|
+
if (!options.version)
|
|
196
|
+
throw Error("[browser-extension] createSafariUniversalProject needs a version to build the extension");
|
|
197
|
+
await buildExtension(config, { target: "safari", version: options.version, cwd });
|
|
198
|
+
}
|
|
199
|
+
const outdir = resolve(cwd, options.outdir ?? resolveOutdir(config, "safari"));
|
|
200
|
+
if (!existsSync(join(outdir, "manifest.json")))
|
|
201
|
+
throw Error(`[browser-extension] ${outdir}/manifest.json is missing. Run extension:build --target safari first.`);
|
|
202
|
+
const appName = safariAppName(config), bundleId = config.safariBundleId;
|
|
203
|
+
if (!bundleId)
|
|
204
|
+
throw Error("[browser-extension] universal Safari packaging needs safariBundleId in config/extension.ts");
|
|
205
|
+
const platforms = resolveSafariPlatforms(config, options.platforms);
|
|
206
|
+
if (!platforms.length)
|
|
207
|
+
throw Error("[browser-extension] universal Safari packaging needs at least one platform");
|
|
208
|
+
const buildDir = join(safariProjectDir(cwd, options.dir), "build"), resources = join(buildDir, "universal-resources"), projectLocation = join(buildDir, "universal-project");
|
|
209
|
+
await mirrorSafariResources(config, outdir, resources);
|
|
210
|
+
await rm(projectLocation, { recursive: !0, force: !0 });
|
|
211
|
+
await mkdir(projectLocation, { recursive: !0 });
|
|
212
|
+
const args = safariPackagerArgs(resources, projectLocation, appName, bundleId, platforms), exitCode = await Bun.spawn(["xcrun", ...args], { cwd, stdout: "inherit", stderr: "inherit" }).exited;
|
|
213
|
+
if (exitCode !== 0)
|
|
214
|
+
throw Error(`[browser-extension] Safari web extension packager failed (${exitCode})`);
|
|
215
|
+
const dir = join(projectLocation, appName), project = join(dir, `${appName}.xcodeproj`);
|
|
216
|
+
if (!existsSync(project))
|
|
217
|
+
throw Error(`[browser-extension] Safari packager did not create ${project}`);
|
|
218
|
+
return { dir, project, resources, platforms };
|
|
219
|
+
}
|
|
220
|
+
export async function buildSafariUniversalApp(config, options = {}) {
|
|
221
|
+
const generated = await createSafariUniversalProject(config, options), appPaths = {};
|
|
222
|
+
if (options.skipXcodebuild)
|
|
223
|
+
return { ...generated, appPaths };
|
|
224
|
+
const appName = safariAppName(config), configuration = options.release ? "Release" : "Debug", derivedData = join(safariProjectDir(options.cwd ?? process.cwd(), options.dir), "build", "universal-derived");
|
|
225
|
+
await rm(derivedData, { recursive: !0, force: !0 });
|
|
226
|
+
for (const platform of generated.platforms) {
|
|
227
|
+
const ios = platform === "ios", args = [
|
|
228
|
+
"xcodebuild",
|
|
229
|
+
"-project",
|
|
230
|
+
generated.project,
|
|
231
|
+
"-scheme",
|
|
232
|
+
`${appName} (${ios ? "iOS" : "macOS"})`,
|
|
233
|
+
"-configuration",
|
|
234
|
+
configuration,
|
|
235
|
+
"-destination",
|
|
236
|
+
ios ? "generic/platform=iOS Simulator" : "generic/platform=macOS",
|
|
237
|
+
"-derivedDataPath",
|
|
238
|
+
derivedData,
|
|
239
|
+
...ios ? ["-sdk", "iphonesimulator", "CODE_SIGNING_ALLOWED=NO"] : options.signed ? ["-allowProvisioningUpdates"] : ["CODE_SIGNING_ALLOWED=NO"],
|
|
240
|
+
"build"
|
|
241
|
+
], exitCode = await Bun.spawn(args, { cwd: generated.dir, stdout: "inherit", stderr: "inherit" }).exited;
|
|
242
|
+
if (exitCode !== 0)
|
|
243
|
+
throw Error(`[browser-extension] Safari ${platform} app build failed (${exitCode})`);
|
|
244
|
+
const products = ios ? `${configuration}-iphonesimulator` : configuration;
|
|
245
|
+
appPaths[platform] = join(derivedData, "Build", "Products", products, `${appName}.app`);
|
|
246
|
+
}
|
|
247
|
+
return { ...generated, appPaths };
|
|
162
248
|
}
|
|
163
249
|
export async function buildSafariApp(config, options = {}) {
|
|
164
250
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -205,6 +291,11 @@ function xcodeAuthArgs(auth) {
|
|
|
205
291
|
auth.issuerId
|
|
206
292
|
];
|
|
207
293
|
}
|
|
294
|
+
async function runXcodebuild(args, cwd, operation) {
|
|
295
|
+
const exitCode = await Bun.spawn(["xcodebuild", ...args], { cwd, stdout: "inherit", stderr: "inherit" }).exited;
|
|
296
|
+
if (exitCode !== 0)
|
|
297
|
+
throw Error(`[browser-extension] ${operation} failed (${exitCode})`);
|
|
298
|
+
}
|
|
208
299
|
function exportOptionsPlist(method, teamId) {
|
|
209
300
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
210
301
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -233,17 +324,76 @@ export async function publishSafariApp(config, options) {
|
|
|
233
324
|
const auth = resolveAppStoreConnectAuth(options), buildNumber = options.buildNumber ?? process.env.GITHUB_RUN_NUMBER ?? String(Math.floor(Date.now() / 1000));
|
|
234
325
|
if (!/^\d+(?:\.\d+){0,2}$/.test(buildNumber))
|
|
235
326
|
throw Error(`[browser-extension] invalid Safari build number ${buildNumber}; use one to three dot-separated integers`);
|
|
327
|
+
const platforms = resolveSafariPlatforms(config, options.platforms);
|
|
328
|
+
if (!platforms.length)
|
|
329
|
+
throw Error("[browser-extension] Safari publishing needs at least one platform");
|
|
330
|
+
const { provisionSafariApp } = await import("./app-store-connect");
|
|
331
|
+
if (!(await provisionSafariApp(config, {
|
|
332
|
+
...auth,
|
|
333
|
+
version: options.version,
|
|
334
|
+
platforms
|
|
335
|
+
})).appRecord.exists)
|
|
336
|
+
throw Error("[browser-extension] the App Store Connect app record is missing; create it once in App Store Connect before publishing");
|
|
236
337
|
if (options.build !== !1)
|
|
237
338
|
await buildExtension(config, { target: "safari", version: options.version, cwd });
|
|
238
|
-
|
|
239
|
-
|
|
339
|
+
const appName = safariAppName(config), dir = safariProjectDir(cwd, options.dir), buildDir = join(dir, "build"), universal = platforms.includes("ios");
|
|
340
|
+
let project, projectCwd;
|
|
341
|
+
if (universal) {
|
|
342
|
+
const generated = await createSafariUniversalProject(config, {
|
|
343
|
+
...options,
|
|
344
|
+
build: !1,
|
|
345
|
+
platforms
|
|
346
|
+
});
|
|
347
|
+
project = generated.project;
|
|
348
|
+
projectCwd = generated.dir;
|
|
349
|
+
} else {
|
|
350
|
+
await syncSafariResources(config, options);
|
|
351
|
+
project = join(dir, `${appName}.xcodeproj`);
|
|
352
|
+
projectCwd = dir;
|
|
353
|
+
}
|
|
354
|
+
const authArgs = xcodeAuthArgs(auth);
|
|
240
355
|
await mkdir(buildDir, { recursive: !0 });
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
356
|
+
const method = options.validateOnly ? "validation" : "app-store-connect", artifacts = [];
|
|
357
|
+
for (const platform of platforms) {
|
|
358
|
+
const suffix = universal ? `-${platform}` : "", archivePath = join(buildDir, `${appName}${suffix}.xcarchive`), exportPath = join(buildDir, `${options.validateOnly ? "validation" : "upload"}${suffix}`), plistPath = join(buildDir, `ExportOptions.${method}${suffix}.plist`), scheme = universal ? `${appName} (${platform === "ios" ? "iOS" : "macOS"})` : appName, destination = platform === "ios" ? "generic/platform=iOS" : "generic/platform=macOS";
|
|
359
|
+
await rm(archivePath, { recursive: !0, force: !0 });
|
|
360
|
+
await rm(exportPath, { recursive: !0, force: !0 });
|
|
361
|
+
await runXcodebuild([
|
|
362
|
+
"-project",
|
|
363
|
+
project,
|
|
364
|
+
"-scheme",
|
|
365
|
+
scheme,
|
|
366
|
+
"-configuration",
|
|
367
|
+
"Release",
|
|
368
|
+
"-destination",
|
|
369
|
+
destination,
|
|
370
|
+
"-archivePath",
|
|
371
|
+
archivePath,
|
|
372
|
+
`MARKETING_VERSION=${options.version}`,
|
|
373
|
+
`CURRENT_PROJECT_VERSION=${buildNumber}`,
|
|
374
|
+
`DEVELOPMENT_TEAM=${teamId}`,
|
|
375
|
+
...platform === "ios" ? ["CODE_SIGNING_ALLOWED=NO"] : [],
|
|
376
|
+
...platform === "macos" && config.safariAppCategory ? [`INFOPLIST_KEY_LSApplicationCategoryType=${config.safariAppCategory}`] : [],
|
|
377
|
+
...authArgs,
|
|
378
|
+
"archive"
|
|
379
|
+
], projectCwd, `Safari ${platform} archive`);
|
|
380
|
+
await Bun.write(plistPath, exportOptionsPlist(method, teamId));
|
|
381
|
+
await runXcodebuild([
|
|
382
|
+
"-exportArchive",
|
|
383
|
+
"-archivePath",
|
|
384
|
+
archivePath,
|
|
385
|
+
"-exportPath",
|
|
386
|
+
exportPath,
|
|
387
|
+
"-exportOptionsPlist",
|
|
388
|
+
plistPath,
|
|
389
|
+
...authArgs
|
|
390
|
+
], projectCwd, `Safari ${platform} export`);
|
|
391
|
+
artifacts.push({ platform, archivePath, exportPath });
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
archivePath: artifacts[0].archivePath,
|
|
395
|
+
exportPath: artifacts[0].exportPath,
|
|
396
|
+
buildNumber,
|
|
397
|
+
artifacts
|
|
398
|
+
};
|
|
249
399
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -76,6 +76,8 @@ export declare interface ExtensionConfig {
|
|
|
76
76
|
firefoxAddons?: FirefoxAddonsConfig
|
|
77
77
|
safariBundleId?: string
|
|
78
78
|
safariTeamId?: string
|
|
79
|
+
safariPlatforms?: SafariPlatform[]
|
|
80
|
+
safariAppCategory?: string
|
|
79
81
|
safariExclude?: string[]
|
|
80
82
|
targets?: ExtensionTarget[]
|
|
81
83
|
background?: string
|
|
@@ -107,3 +109,4 @@ export declare interface BuildOptions {
|
|
|
107
109
|
* or per-project build script.
|
|
108
110
|
*/
|
|
109
111
|
export type ExtensionTarget = 'chrome' | 'firefox' | 'safari';
|
|
112
|
+
export type SafariPlatform = 'macos' | 'ios';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/browser-extension",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.70.
|
|
4
|
+
"version": "0.70.142",
|
|
5
5
|
"description": "Build MV3 browser extensions (Chrome, Firefox, Safari) the Stacks way — manifest, content/background scripts, DNR rules, packaging, Safari container app, all config-driven.",
|
|
6
6
|
"author": "Chris Breuer",
|
|
7
7
|
"contributors": [
|