@stacksjs/browser-extension 0.70.258 → 0.70.259

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.
@@ -1,49 +1 @@
1
- import {
2
- AppStoreConnectClient,
3
- ensureAppStoreVersions,
4
- provisionMacApp,
5
- waitForAppStoreBuilds
6
- } from "ts-pantry";
7
-
8
- export {
9
- AppStoreConnectClient,
10
- provisionMacApp
11
- };
12
- export { appStoreConnectToken } from "ts-pantry";
13
- function appStorePlatform(platform) {
14
- return platform === "ios" ? "IOS" : "MAC_OS";
15
- }
16
- export async function attachSafariBuilds(client, appId, versions, buildNumber, options = {}) {
17
- const pantryVersions = versions.map((version) => ({
18
- platform: appStorePlatform(version.platform),
19
- version: version.version,
20
- id: version.id
21
- }));
22
- return (await waitForAppStoreBuilds(client, appId, pantryVersions, buildNumber, options)).map((attachment) => ({
23
- platform: attachment.platform === "IOS" ? "ios" : "macos",
24
- versionId: attachment.versionId,
25
- buildId: attachment.buildId,
26
- buildNumber: attachment.buildNumber
27
- }));
28
- }
29
- async function ensureConfiguredAppStoreVersions(client, appId, platforms, version) {
30
- return (await ensureAppStoreVersions(client, appId, platforms.map((platform) => ({ platform: appStorePlatform(platform), version })))).map((result) => {
31
- const platform = result.platform === "IOS" ? "ios" : "macos";
32
- return {
33
- ...result,
34
- platform,
35
- ...result.reason ? { reason: result.reason.replace(`${result.platform} version`, `Safari ${platform} version`) } : {}
36
- };
37
- });
38
- }
39
- export async function provisionSafariApp(config, options = {}) {
40
- if (!config.safariBundleId)
41
- throw Error("[browser-extension] Safari provisioning needs safariBundleId in config/extension.ts");
42
- 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) : [];
43
- return {
44
- container: { identifier, exists: Boolean(container.bundleId), created: container.created },
45
- extension: { identifier: extensionIdentifier, exists: Boolean(extension.bundleId), created: extension.created },
46
- appRecord: { exists: Boolean(app), id: app?.id },
47
- appStoreVersions
48
- };
49
- }
1
+ import{AppStoreConnectClient,ensureAppStoreVersions,provisionMacApp,waitForAppStoreBuilds}from"ts-pantry";export{AppStoreConnectClient,provisionMacApp};export{appStoreConnectToken}from"ts-pantry";function appStorePlatform(platform){return platform==="ios"?"IOS":"MAC_OS"}export async function attachSafariBuilds(client,appId,versions,buildNumber,options={}){const pantryVersions=versions.map((version)=>({platform:appStorePlatform(version.platform),version:version.version,id:version.id}));return(await waitForAppStoreBuilds(client,appId,pantryVersions,buildNumber,options)).map((attachment)=>({platform:attachment.platform==="IOS"?"ios":"macos",versionId:attachment.versionId,buildId:attachment.buildId,buildNumber:attachment.buildNumber}))}async function ensureConfiguredAppStoreVersions(client,appId,platforms,version){return(await ensureAppStoreVersions(client,appId,platforms.map((platform)=>({platform:appStorePlatform(platform),version})))).map((result)=>{const platform=result.platform==="IOS"?"ios":"macos";return{...result,platform,...result.reason?{reason:result.reason.replace(`${result.platform} version`,`Safari ${platform} version`)}:{}}})}export async function provisionSafariApp(config,options={}){if(!config.safariBundleId)throw Error("[browser-extension] Safari provisioning needs safariBundleId in config/extension.ts");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):[];return{container:{identifier,exists:Boolean(container.bundleId),created:container.created},extension:{identifier:extensionIdentifier,exists:Boolean(extension.bundleId),created:extension.created},appRecord:{exists:Boolean(app),id:app?.id},appStoreVersions}}
@@ -1,443 +1 @@
1
- import { createHash } from "node:crypto";
2
- import { basename, resolve } from "node:path";
3
- import { readFile, stat } from "node:fs/promises";
4
- import { AppStoreConnectClient } from "./app-store-connect";
5
- const frequencyFields = [
6
- "alcoholTobaccoOrDrugUseOrReferences",
7
- "contests",
8
- "gamblingSimulated",
9
- "gunsOrOtherWeapons",
10
- "medicalOrTreatmentInformation",
11
- "profanityOrCrudeHumor",
12
- "sexualContentGraphicAndNudity",
13
- "sexualContentOrNudity",
14
- "horrorOrFearThemes",
15
- "matureOrSuggestiveThemes",
16
- "violenceCartoonOrFantasy",
17
- "violenceRealisticProlongedGraphicOrSadistic",
18
- "violenceRealistic"
19
- ], booleanFields = [
20
- "advertising",
21
- "gambling",
22
- "healthOrWellnessTopics",
23
- "lootBox",
24
- "messagingAndChat",
25
- "parentalControls",
26
- "ageAssurance",
27
- "socialMedia",
28
- "socialMediaAgeRestricted",
29
- "unrestrictedWebAccess",
30
- "userGeneratedContent"
31
- ], editableVersionStates = new Set(["PREPARE_FOR_SUBMISSION", "DEVELOPER_REJECTED", "REJECTED", "METADATA_REJECTED"]);
32
- function ageRatingAttributes(config = {}) {
33
- const attributes = {
34
- ageRatingOverrideV2: "NONE",
35
- koreaAgeRatingOverride: "NONE"
36
- };
37
- for (const field of frequencyFields)
38
- attributes[field] = config[field] ?? "NONE";
39
- for (const field of booleanFields)
40
- attributes[field] = config[field] ?? !1;
41
- return attributes;
42
- }
43
- function applePlatform(platform) {
44
- return platform === "ios" ? "IOS" : "MAC_OS";
45
- }
46
- function displayTypesForPlatform(platform) {
47
- return platform === "macos" ? ["APP_DESKTOP"] : ["APP_IPHONE_67", "APP_IPAD_PRO_3GEN_129"];
48
- }
49
- async function updateAppInfo(client, appId, config) {
50
- await client.request(`/apps/${appId}`, {
51
- method: "PATCH",
52
- body: JSON.stringify({
53
- data: {
54
- type: "apps",
55
- id: appId,
56
- attributes: { contentRightsDeclaration: config.contentRightsDeclaration }
57
- }
58
- })
59
- });
60
- const infos = await client.request(`/apps/${appId}/appInfos?limit=10`), info = infos.data.find((item) => item.attributes.appStoreState === "PREPARE_FOR_SUBMISSION") ?? infos.data[0];
61
- if (!info)
62
- throw Error("[browser-extension] App Store Connect has no editable app information resource");
63
- await client.request(`/appInfos/${info.id}`, {
64
- method: "PATCH",
65
- body: JSON.stringify({
66
- data: {
67
- type: "appInfos",
68
- id: info.id,
69
- relationships: {
70
- primaryCategory: { data: { type: "appCategories", id: config.primaryCategory } }
71
- }
72
- }
73
- })
74
- });
75
- const localizations = await client.request(`/appInfos/${info.id}/appInfoLocalizations?limit=200`), locale = config.locale ?? "en-US", localization = localizations.data.find((item) => item.attributes.locale === locale);
76
- if (!localization)
77
- throw Error(`[browser-extension] App Store Connect app information is missing the ${locale} localization`);
78
- await client.request(`/appInfoLocalizations/${localization.id}`, {
79
- method: "PATCH",
80
- body: JSON.stringify({
81
- data: {
82
- type: "appInfoLocalizations",
83
- id: localization.id,
84
- attributes: {
85
- subtitle: config.subtitle,
86
- privacyPolicyUrl: config.privacyPolicyUrl
87
- }
88
- }
89
- })
90
- });
91
- await client.request(`/ageRatingDeclarations/${info.id}`, {
92
- method: "PATCH",
93
- body: JSON.stringify({
94
- data: {
95
- type: "ageRatingDeclarations",
96
- id: info.id,
97
- attributes: ageRatingAttributes(config.ageRating)
98
- }
99
- })
100
- });
101
- return info.id;
102
- }
103
- async function ensurePrice(client, appId, config) {
104
- if ((await client.request(`/appPriceSchedules/${appId}/manualPrices?limit=200`)).data.length)
105
- return;
106
- const baseTerritory = config.baseTerritory ?? "USA", pricePoints = await client.request(`/apps/${appId}/appPricePoints?filter[territory]=${encodeURIComponent(baseTerritory)}&limit=200`), requestedPrice = Number(config.price), pricePoint = pricePoints.data.find((item) => Number(item.attributes.customerPrice) === requestedPrice);
107
- if (!pricePoint)
108
- throw Error(`[browser-extension] App Store price ${config.price} is unavailable in ${baseTerritory}`);
109
- const localId = "${price}";
110
- await client.request("/appPriceSchedules", {
111
- method: "POST",
112
- body: JSON.stringify({
113
- data: {
114
- type: "appPriceSchedules",
115
- relationships: {
116
- app: { data: { type: "apps", id: appId } },
117
- baseTerritory: { data: { type: "territories", id: baseTerritory } },
118
- manualPrices: { data: [{ type: "appPrices", id: localId }] }
119
- }
120
- },
121
- included: [{
122
- type: "appPrices",
123
- id: localId,
124
- attributes: { startDate: null, endDate: null },
125
- relationships: { appPricePoint: { data: { type: "appPricePoints", id: pricePoint.id } } }
126
- }]
127
- })
128
- });
129
- }
130
- async function ensureAvailability(client, appId, availableInNewTerritories = !0) {
131
- try {
132
- if ((await client.request(`/apps/${appId}/appAvailabilityV2`)).data.attributes.availableInNewTerritories === availableInNewTerritories)
133
- return;
134
- return;
135
- } catch (error) {
136
- if (!(error instanceof Error) || !error.message.includes("(404)"))
137
- throw error;
138
- }
139
- const included = (await client.request("/territories?limit=200")).data.map((territory, index) => {
140
- return {
141
- type: "territoryAvailabilities",
142
- id: `\${territory-${index}}`,
143
- attributes: { available: !0 },
144
- relationships: { territory: { data: { type: "territories", id: territory.id } } }
145
- };
146
- });
147
- await client.request("/v2/appAvailabilities", {
148
- method: "POST",
149
- body: JSON.stringify({
150
- data: {
151
- type: "appAvailabilities",
152
- attributes: { availableInNewTerritories },
153
- relationships: {
154
- app: { data: { type: "apps", id: appId } },
155
- territoryAvailabilities: {
156
- data: included.map((item) => ({ type: item.type, id: item.id }))
157
- }
158
- }
159
- },
160
- included
161
- })
162
- });
163
- }
164
- async function ensureVersionLocalization(client, versionId, config) {
165
- const locale = config.locale ?? "en-US";
166
- let localization = (await client.request(`/appStoreVersions/${versionId}/appStoreVersionLocalizations?limit=200`)).data.find((item) => item.attributes.locale === locale);
167
- if (!localization)
168
- localization = (await client.request("/appStoreVersionLocalizations", {
169
- method: "POST",
170
- body: JSON.stringify({
171
- data: {
172
- type: "appStoreVersionLocalizations",
173
- attributes: { locale },
174
- relationships: { appStoreVersion: { data: { type: "appStoreVersions", id: versionId } } }
175
- }
176
- })
177
- })).data;
178
- await client.request(`/appStoreVersionLocalizations/${localization.id}`, {
179
- method: "PATCH",
180
- body: JSON.stringify({
181
- data: {
182
- type: "appStoreVersionLocalizations",
183
- id: localization.id,
184
- attributes: {
185
- description: config.description,
186
- keywords: config.keywords,
187
- supportUrl: config.supportUrl,
188
- ...config.marketingUrl ? { marketingUrl: config.marketingUrl } : {},
189
- ...config.promotionalText ? { promotionalText: config.promotionalText } : {},
190
- ...config.whatsNew ? { whatsNew: config.whatsNew } : {}
191
- }
192
- }
193
- })
194
- });
195
- return localization.id;
196
- }
197
- async function ensureReviewDetail(client, versionId, config) {
198
- const response = await client.request(`/appStoreVersions/${versionId}/appStoreReviewDetail`), attributes = {
199
- contactFirstName: config.reviewContact.firstName,
200
- contactLastName: config.reviewContact.lastName,
201
- contactPhone: config.reviewContact.phone,
202
- contactEmail: config.reviewContact.email,
203
- demoAccountRequired: !1,
204
- ...config.reviewContact.notes ? { notes: config.reviewContact.notes } : {}
205
- };
206
- if (response.data) {
207
- await client.request(`/appStoreReviewDetails/${response.data.id}`, {
208
- method: "PATCH",
209
- body: JSON.stringify({ data: { type: "appStoreReviewDetails", id: response.data.id, attributes } })
210
- });
211
- return;
212
- }
213
- await client.request("/appStoreReviewDetails", {
214
- method: "POST",
215
- body: JSON.stringify({
216
- data: {
217
- type: "appStoreReviewDetails",
218
- attributes,
219
- relationships: { appStoreVersion: { data: { type: "appStoreVersions", id: versionId } } }
220
- }
221
- })
222
- });
223
- }
224
- async function ensureScreenshotSet(client, localizationId, displayType) {
225
- const existing = (await client.request(`/appStoreVersionLocalizations/${localizationId}/appScreenshotSets?limit=200`)).data.find((item) => item.attributes.screenshotDisplayType === displayType);
226
- if (existing)
227
- return existing.id;
228
- return (await client.request("/appScreenshotSets", {
229
- method: "POST",
230
- body: JSON.stringify({
231
- data: {
232
- type: "appScreenshotSets",
233
- attributes: { screenshotDisplayType: displayType },
234
- relationships: {
235
- appStoreVersionLocalization: { data: { type: "appStoreVersionLocalizations", id: localizationId } }
236
- }
237
- }
238
- })
239
- })).data.id;
240
- }
241
- async function waitForScreenshot(client, id, timeoutMs, pollIntervalMs) {
242
- const deadline = Date.now() + timeoutMs;
243
- while (!0) {
244
- const response = await client.request(`/appScreenshots/${id}`), state = response.data.attributes.assetDeliveryState?.state;
245
- if (state === "COMPLETE")
246
- return;
247
- if (state === "FAILED") {
248
- const detail = response.data.attributes.assetDeliveryState?.errors?.map((error) => error.message).filter(Boolean).join("; ");
249
- throw Error(`[browser-extension] App Store screenshot processing failed${detail ? `: ${detail}` : ""}`);
250
- }
251
- if (Date.now() >= deadline)
252
- throw Error(`[browser-extension] timed out waiting for App Store screenshot ${response.data.attributes.fileName}`);
253
- await Bun.sleep(pollIntervalMs);
254
- }
255
- }
256
- async function uploadScreenshot(client, setId, path, timeoutMs, pollIntervalMs) {
257
- const bytes = await readFile(path), file = basename(path), checksum = createHash("md5").update(bytes).digest("hex"), reserved = await client.request("/appScreenshots", {
258
- method: "POST",
259
- body: JSON.stringify({
260
- data: {
261
- type: "appScreenshots",
262
- attributes: { fileName: file, fileSize: bytes.byteLength },
263
- relationships: { appScreenshotSet: { data: { type: "appScreenshotSets", id: setId } } }
264
- }
265
- })
266
- });
267
- for (const operation of reserved.data.attributes.uploadOperations ?? []) {
268
- const body = bytes.subarray(operation.offset, operation.offset + operation.length);
269
- await client.upload(operation.url, {
270
- method: operation.method,
271
- headers: Object.fromEntries(operation.requestHeaders.map((header) => [header.name, header.value])),
272
- body
273
- });
274
- }
275
- await client.request(`/appScreenshots/${reserved.data.id}`, {
276
- method: "PATCH",
277
- body: JSON.stringify({
278
- data: {
279
- type: "appScreenshots",
280
- id: reserved.data.id,
281
- attributes: { uploaded: !0, sourceFileChecksum: checksum }
282
- }
283
- })
284
- });
285
- await waitForScreenshot(client, reserved.data.id, timeoutMs, pollIntervalMs);
286
- return reserved.data.id;
287
- }
288
- async function syncScreenshotSet(client, setId, paths, cwd, timeoutMs, pollIntervalMs) {
289
- if (paths.length > 10)
290
- throw Error("[browser-extension] App Store screenshot sets accept at most 10 images");
291
- const files = await Promise.all(paths.map(async (path) => {
292
- const absolute = resolve(cwd, path);
293
- await stat(absolute);
294
- const bytes = await readFile(absolute);
295
- return { absolute, name: basename(absolute), checksum: createHash("md5").update(bytes).digest("hex") };
296
- })), existing = await client.request(`/appScreenshotSets/${setId}/appScreenshots?limit=200`);
297
- for (const screenshot of existing.data) {
298
- const desired = files.find((file) => file.name === screenshot.attributes.fileName);
299
- if (!desired || desired.checksum !== screenshot.attributes.sourceFileChecksum)
300
- await client.request(`/appScreenshots/${screenshot.id}`, { method: "DELETE" });
301
- }
302
- for (const file of files)
303
- if (!existing.data.find((screenshot) => screenshot.attributes.fileName === file.name && screenshot.attributes.sourceFileChecksum === file.checksum))
304
- await uploadScreenshot(client, setId, file.absolute, timeoutMs, pollIntervalMs);
305
- }
306
- async function updateVersion(client, version, config) {
307
- await client.request(`/appStoreVersions/${version.id}`, {
308
- method: "PATCH",
309
- body: JSON.stringify({
310
- data: {
311
- type: "appStoreVersions",
312
- id: version.id,
313
- attributes: {
314
- copyright: config.copyright,
315
- releaseType: config.releaseType ?? "AFTER_APPROVAL",
316
- usesIdfa: config.usesIdfa ?? !1
317
- }
318
- }
319
- })
320
- });
321
- if (version.buildId) {
322
- const encryption = config.usesNonExemptEncryption ?? !1, build = await client.request(`/builds/${version.buildId}?fields[builds]=usesNonExemptEncryption`);
323
- if (build.data.attributes.usesNonExemptEncryption === void 0 || build.data.attributes.usesNonExemptEncryption === null)
324
- await client.request(`/builds/${version.buildId}`, {
325
- method: "PATCH",
326
- body: JSON.stringify({
327
- data: {
328
- type: "builds",
329
- id: version.buildId,
330
- attributes: { usesNonExemptEncryption: encryption }
331
- }
332
- })
333
- });
334
- }
335
- }
336
- async function submitVersions(client, appId, versions) {
337
- const existing = await client.request(`/apps/${appId}/reviewSubmissions?limit=200`), ids = [];
338
- for (const version of versions) {
339
- const platform = applePlatform(version.platform), platformSubmissions = existing.data.filter((item) => item.attributes.platform === platform);
340
- let submission;
341
- for (const candidate of platformSubmissions) {
342
- if (!((await client.request(`/reviewSubmissions/${candidate.id}/items?include=appStoreVersion&limit=200`)).included ?? []).some((item) => item.type === "appStoreVersions" && item.id === version.id))
343
- continue;
344
- submission = candidate;
345
- if (candidate.attributes.state !== "READY_FOR_REVIEW") {
346
- ids.push(candidate.id);
347
- break;
348
- }
349
- }
350
- if (submission && submission.attributes.state !== "READY_FOR_REVIEW")
351
- continue;
352
- submission ??= platformSubmissions.find((item) => item.attributes.state === "READY_FOR_REVIEW");
353
- if (!submission)
354
- submission = (await client.request("/reviewSubmissions", {
355
- method: "POST",
356
- body: JSON.stringify({
357
- data: {
358
- type: "reviewSubmissions",
359
- attributes: { platform },
360
- relationships: { app: { data: { type: "apps", id: appId } } }
361
- }
362
- })
363
- })).data;
364
- if (!((await client.request(`/reviewSubmissions/${submission.id}/items?include=appStoreVersion&limit=200`)).included ?? []).some((item) => item.type === "appStoreVersions" && item.id === version.id))
365
- await client.request("/reviewSubmissionItems", {
366
- method: "POST",
367
- body: JSON.stringify({
368
- data: {
369
- type: "reviewSubmissionItems",
370
- relationships: {
371
- reviewSubmission: { data: { type: "reviewSubmissions", id: submission.id } },
372
- appStoreVersion: { data: { type: "appStoreVersions", id: version.id } }
373
- }
374
- }
375
- })
376
- });
377
- await client.request(`/reviewSubmissions/${submission.id}`, {
378
- method: "PATCH",
379
- body: JSON.stringify({
380
- data: { type: "reviewSubmissions", id: submission.id, attributes: { submitted: !0 } }
381
- })
382
- });
383
- ids.push(submission.id);
384
- }
385
- return ids;
386
- }
387
- export async function prepareSafariAppStoreSubmission(client, config, appId, versions, options = {}) {
388
- const store = config.safariAppStore;
389
- if (!store)
390
- throw Error("[browser-extension] Safari App Store automation needs safariAppStore in config/extension.ts");
391
- const cwd = options.cwd ?? process.cwd(), timeoutMs = options.screenshotTimeoutMs ?? 600000, pollIntervalMs = options.screenshotPollIntervalMs ?? 5000;
392
- await updateAppInfo(client, appId, store);
393
- await ensurePrice(client, appId, store);
394
- await ensureAvailability(client, appId, store.availableInNewTerritories ?? !0);
395
- const results = [];
396
- for (const version of versions) {
397
- await updateVersion(client, version, store);
398
- const localizationId = await ensureVersionLocalization(client, version.id, store);
399
- await ensureReviewDetail(client, version.id, store);
400
- for (const displayType of displayTypesForPlatform(version.platform)) {
401
- const paths = store.screenshots[displayType] ?? [];
402
- if (!paths.length)
403
- continue;
404
- const setId = await ensureScreenshotSet(client, localizationId, displayType);
405
- await syncScreenshotSet(client, setId, paths, cwd, timeoutMs, pollIntervalMs);
406
- }
407
- results.push({ platform: version.platform, versionId: version.id, localizationId });
408
- }
409
- const shouldSubmit = options.submit ?? store.submitForReview ?? !1;
410
- return {
411
- appId,
412
- versions: results,
413
- reviewSubmissionIds: shouldSubmit ? await submitVersions(client, appId, versions) : []
414
- };
415
- }
416
- export async function submitSafariAppStore(config, options) {
417
- if (!config.safariBundleId)
418
- throw Error("[browser-extension] Safari App Store submission needs safariBundleId in config/extension.ts");
419
- const client = new AppStoreConnectClient(options), app = await client.findApp(config.safariBundleId);
420
- if (!app)
421
- throw Error("[browser-extension] the App Store Connect app record is missing");
422
- const selectedPlatforms = options.platforms ?? config.safariPlatforms ?? ["macos"], availableVersions = await client.listAppStoreVersions(app.id), versions = [], versionStates = [];
423
- for (const platform of selectedPlatforms) {
424
- const match = availableVersions.find((item) => item.attributes.platform === applePlatform(platform) && item.attributes.versionString === options.version);
425
- if (!match)
426
- throw Error(`[browser-extension] Safari ${platform} App Store version ${options.version} does not exist`);
427
- const build = await client.request(`/appStoreVersions/${match.id}/build`);
428
- versions.push({ platform, id: match.id, buildId: build.data?.id });
429
- versionStates.push(match.attributes.appStoreState);
430
- }
431
- if ((options.submit ?? config.safariAppStore?.submitForReview ?? !1) && versionStates.every((state) => !editableVersionStates.has(state))) {
432
- const preparedVersions = await Promise.all(versions.map(async (version) => {
433
- const localizations = await client.request(`/appStoreVersions/${version.id}/appStoreVersionLocalizations?limit=200`);
434
- return { platform: version.platform, versionId: version.id, localizationId: localizations.data[0]?.id ?? "" };
435
- }));
436
- return {
437
- appId: app.id,
438
- versions: preparedVersions,
439
- reviewSubmissionIds: await submitVersions(client, app.id, versions)
440
- };
441
- }
442
- return await prepareSafariAppStoreSubmission(client, config, app.id, versions, options);
443
- }
1
+ import{createHash}from"node:crypto";import{basename,resolve}from"node:path";import{readFile,stat}from"node:fs/promises";import{AppStoreConnectClient}from"./app-store-connect";const frequencyFields=["alcoholTobaccoOrDrugUseOrReferences","contests","gamblingSimulated","gunsOrOtherWeapons","medicalOrTreatmentInformation","profanityOrCrudeHumor","sexualContentGraphicAndNudity","sexualContentOrNudity","horrorOrFearThemes","matureOrSuggestiveThemes","violenceCartoonOrFantasy","violenceRealisticProlongedGraphicOrSadistic","violenceRealistic"],booleanFields=["advertising","gambling","healthOrWellnessTopics","lootBox","messagingAndChat","parentalControls","ageAssurance","socialMedia","socialMediaAgeRestricted","unrestrictedWebAccess","userGeneratedContent"],editableVersionStates=new Set(["PREPARE_FOR_SUBMISSION","DEVELOPER_REJECTED","REJECTED","METADATA_REJECTED"]);function ageRatingAttributes(config={}){const attributes={ageRatingOverrideV2:"NONE",koreaAgeRatingOverride:"NONE"};for(const field of frequencyFields)attributes[field]=config[field]??"NONE";for(const field of booleanFields)attributes[field]=config[field]??!1;return attributes}function applePlatform(platform){return platform==="ios"?"IOS":"MAC_OS"}function displayTypesForPlatform(platform){return platform==="macos"?["APP_DESKTOP"]:["APP_IPHONE_67","APP_IPAD_PRO_3GEN_129"]}async function updateAppInfo(client,appId,config){await client.request(`/apps/${appId}`,{method:"PATCH",body:JSON.stringify({data:{type:"apps",id:appId,attributes:{contentRightsDeclaration:config.contentRightsDeclaration}}})});const infos=await client.request(`/apps/${appId}/appInfos?limit=10`),info=infos.data.find((item)=>item.attributes.appStoreState==="PREPARE_FOR_SUBMISSION")??infos.data[0];if(!info)throw Error("[browser-extension] App Store Connect has no editable app information resource");await client.request(`/appInfos/${info.id}`,{method:"PATCH",body:JSON.stringify({data:{type:"appInfos",id:info.id,relationships:{primaryCategory:{data:{type:"appCategories",id:config.primaryCategory}}}}})});const localizations=await client.request(`/appInfos/${info.id}/appInfoLocalizations?limit=200`),locale=config.locale??"en-US",localization=localizations.data.find((item)=>item.attributes.locale===locale);if(!localization)throw Error(`[browser-extension] App Store Connect app information is missing the ${locale} localization`);await client.request(`/appInfoLocalizations/${localization.id}`,{method:"PATCH",body:JSON.stringify({data:{type:"appInfoLocalizations",id:localization.id,attributes:{subtitle:config.subtitle,privacyPolicyUrl:config.privacyPolicyUrl}}})});await client.request(`/ageRatingDeclarations/${info.id}`,{method:"PATCH",body:JSON.stringify({data:{type:"ageRatingDeclarations",id:info.id,attributes:ageRatingAttributes(config.ageRating)}})});return info.id}async function ensurePrice(client,appId,config){if((await client.request(`/appPriceSchedules/${appId}/manualPrices?limit=200`)).data.length)return;const baseTerritory=config.baseTerritory??"USA",pricePoints=await client.request(`/apps/${appId}/appPricePoints?filter[territory]=${encodeURIComponent(baseTerritory)}&limit=200`),requestedPrice=Number(config.price),pricePoint=pricePoints.data.find((item)=>Number(item.attributes.customerPrice)===requestedPrice);if(!pricePoint)throw Error(`[browser-extension] App Store price ${config.price} is unavailable in ${baseTerritory}`);const localId="${price}";await client.request("/appPriceSchedules",{method:"POST",body:JSON.stringify({data:{type:"appPriceSchedules",relationships:{app:{data:{type:"apps",id:appId}},baseTerritory:{data:{type:"territories",id:baseTerritory}},manualPrices:{data:[{type:"appPrices",id:localId}]}}},included:[{type:"appPrices",id:localId,attributes:{startDate:null,endDate:null},relationships:{appPricePoint:{data:{type:"appPricePoints",id:pricePoint.id}}}}]})})}async function ensureAvailability(client,appId,availableInNewTerritories=!0){try{if((await client.request(`/apps/${appId}/appAvailabilityV2`)).data.attributes.availableInNewTerritories===availableInNewTerritories)return;return}catch(error){if(!(error instanceof Error)||!error.message.includes("(404)"))throw error}const included=(await client.request("/territories?limit=200")).data.map((territory,index)=>{return{type:"territoryAvailabilities",id:`\${territory-${index}}`,attributes:{available:!0},relationships:{territory:{data:{type:"territories",id:territory.id}}}}});await client.request("/v2/appAvailabilities",{method:"POST",body:JSON.stringify({data:{type:"appAvailabilities",attributes:{availableInNewTerritories},relationships:{app:{data:{type:"apps",id:appId}},territoryAvailabilities:{data:included.map((item)=>({type:item.type,id:item.id}))}}},included})})}async function ensureVersionLocalization(client,versionId,config){const locale=config.locale??"en-US";let localization=(await client.request(`/appStoreVersions/${versionId}/appStoreVersionLocalizations?limit=200`)).data.find((item)=>item.attributes.locale===locale);if(!localization)localization=(await client.request("/appStoreVersionLocalizations",{method:"POST",body:JSON.stringify({data:{type:"appStoreVersionLocalizations",attributes:{locale},relationships:{appStoreVersion:{data:{type:"appStoreVersions",id:versionId}}}}})})).data;await client.request(`/appStoreVersionLocalizations/${localization.id}`,{method:"PATCH",body:JSON.stringify({data:{type:"appStoreVersionLocalizations",id:localization.id,attributes:{description:config.description,keywords:config.keywords,supportUrl:config.supportUrl,...config.marketingUrl?{marketingUrl:config.marketingUrl}:{},...config.promotionalText?{promotionalText:config.promotionalText}:{},...config.whatsNew?{whatsNew:config.whatsNew}:{}}}})});return localization.id}async function ensureReviewDetail(client,versionId,config){const response=await client.request(`/appStoreVersions/${versionId}/appStoreReviewDetail`),attributes={contactFirstName:config.reviewContact.firstName,contactLastName:config.reviewContact.lastName,contactPhone:config.reviewContact.phone,contactEmail:config.reviewContact.email,demoAccountRequired:!1,...config.reviewContact.notes?{notes:config.reviewContact.notes}:{}};if(response.data){await client.request(`/appStoreReviewDetails/${response.data.id}`,{method:"PATCH",body:JSON.stringify({data:{type:"appStoreReviewDetails",id:response.data.id,attributes}})});return}await client.request("/appStoreReviewDetails",{method:"POST",body:JSON.stringify({data:{type:"appStoreReviewDetails",attributes,relationships:{appStoreVersion:{data:{type:"appStoreVersions",id:versionId}}}}})})}async function ensureScreenshotSet(client,localizationId,displayType){const existing=(await client.request(`/appStoreVersionLocalizations/${localizationId}/appScreenshotSets?limit=200`)).data.find((item)=>item.attributes.screenshotDisplayType===displayType);if(existing)return existing.id;return(await client.request("/appScreenshotSets",{method:"POST",body:JSON.stringify({data:{type:"appScreenshotSets",attributes:{screenshotDisplayType:displayType},relationships:{appStoreVersionLocalization:{data:{type:"appStoreVersionLocalizations",id:localizationId}}}}})})).data.id}async function waitForScreenshot(client,id,timeoutMs,pollIntervalMs){const deadline=Date.now()+timeoutMs;while(!0){const response=await client.request(`/appScreenshots/${id}`),state=response.data.attributes.assetDeliveryState?.state;if(state==="COMPLETE")return;if(state==="FAILED"){const detail=response.data.attributes.assetDeliveryState?.errors?.map((error)=>error.message).filter(Boolean).join("; ");throw Error(`[browser-extension] App Store screenshot processing failed${detail?`: ${detail}`:""}`)}if(Date.now()>=deadline)throw Error(`[browser-extension] timed out waiting for App Store screenshot ${response.data.attributes.fileName}`);await Bun.sleep(pollIntervalMs)}}async function uploadScreenshot(client,setId,path,timeoutMs,pollIntervalMs){const bytes=await readFile(path),file=basename(path),checksum=createHash("md5").update(bytes).digest("hex"),reserved=await client.request("/appScreenshots",{method:"POST",body:JSON.stringify({data:{type:"appScreenshots",attributes:{fileName:file,fileSize:bytes.byteLength},relationships:{appScreenshotSet:{data:{type:"appScreenshotSets",id:setId}}}}})});for(const operation of reserved.data.attributes.uploadOperations??[]){const body=bytes.subarray(operation.offset,operation.offset+operation.length);await client.upload(operation.url,{method:operation.method,headers:Object.fromEntries(operation.requestHeaders.map((header)=>[header.name,header.value])),body})}await client.request(`/appScreenshots/${reserved.data.id}`,{method:"PATCH",body:JSON.stringify({data:{type:"appScreenshots",id:reserved.data.id,attributes:{uploaded:!0,sourceFileChecksum:checksum}}})});await waitForScreenshot(client,reserved.data.id,timeoutMs,pollIntervalMs);return reserved.data.id}async function syncScreenshotSet(client,setId,paths,cwd,timeoutMs,pollIntervalMs){if(paths.length>10)throw Error("[browser-extension] App Store screenshot sets accept at most 10 images");const files=await Promise.all(paths.map(async(path)=>{const absolute=resolve(cwd,path);await stat(absolute);const bytes=await readFile(absolute);return{absolute,name:basename(absolute),checksum:createHash("md5").update(bytes).digest("hex")}})),existing=await client.request(`/appScreenshotSets/${setId}/appScreenshots?limit=200`);for(const screenshot of existing.data){const desired=files.find((file)=>file.name===screenshot.attributes.fileName);if(!desired||desired.checksum!==screenshot.attributes.sourceFileChecksum)await client.request(`/appScreenshots/${screenshot.id}`,{method:"DELETE"})}for(const file of files)if(!existing.data.find((screenshot)=>screenshot.attributes.fileName===file.name&&screenshot.attributes.sourceFileChecksum===file.checksum))await uploadScreenshot(client,setId,file.absolute,timeoutMs,pollIntervalMs)}async function updateVersion(client,version,config){await client.request(`/appStoreVersions/${version.id}`,{method:"PATCH",body:JSON.stringify({data:{type:"appStoreVersions",id:version.id,attributes:{copyright:config.copyright,releaseType:config.releaseType??"AFTER_APPROVAL",usesIdfa:config.usesIdfa??!1}}})});if(version.buildId){const encryption=config.usesNonExemptEncryption??!1,build=await client.request(`/builds/${version.buildId}?fields[builds]=usesNonExemptEncryption`);if(build.data.attributes.usesNonExemptEncryption===void 0||build.data.attributes.usesNonExemptEncryption===null)await client.request(`/builds/${version.buildId}`,{method:"PATCH",body:JSON.stringify({data:{type:"builds",id:version.buildId,attributes:{usesNonExemptEncryption:encryption}}})})}}async function submitVersions(client,appId,versions){const existing=await client.request(`/apps/${appId}/reviewSubmissions?limit=200`),ids=[];for(const version of versions){const platform=applePlatform(version.platform),platformSubmissions=existing.data.filter((item)=>item.attributes.platform===platform);let submission;for(const candidate of platformSubmissions){if(!((await client.request(`/reviewSubmissions/${candidate.id}/items?include=appStoreVersion&limit=200`)).included??[]).some((item)=>item.type==="appStoreVersions"&&item.id===version.id))continue;submission=candidate;if(candidate.attributes.state!=="READY_FOR_REVIEW"){ids.push(candidate.id);break}}if(submission&&submission.attributes.state!=="READY_FOR_REVIEW")continue;submission??=platformSubmissions.find((item)=>item.attributes.state==="READY_FOR_REVIEW");if(!submission)submission=(await client.request("/reviewSubmissions",{method:"POST",body:JSON.stringify({data:{type:"reviewSubmissions",attributes:{platform},relationships:{app:{data:{type:"apps",id:appId}}}}})})).data;if(!((await client.request(`/reviewSubmissions/${submission.id}/items?include=appStoreVersion&limit=200`)).included??[]).some((item)=>item.type==="appStoreVersions"&&item.id===version.id))await client.request("/reviewSubmissionItems",{method:"POST",body:JSON.stringify({data:{type:"reviewSubmissionItems",relationships:{reviewSubmission:{data:{type:"reviewSubmissions",id:submission.id}},appStoreVersion:{data:{type:"appStoreVersions",id:version.id}}}}})});await client.request(`/reviewSubmissions/${submission.id}`,{method:"PATCH",body:JSON.stringify({data:{type:"reviewSubmissions",id:submission.id,attributes:{submitted:!0}}})});ids.push(submission.id)}return ids}export async function prepareSafariAppStoreSubmission(client,config,appId,versions,options={}){const store=config.safariAppStore;if(!store)throw Error("[browser-extension] Safari App Store automation needs safariAppStore in config/extension.ts");const cwd=options.cwd??process.cwd(),timeoutMs=options.screenshotTimeoutMs??600000,pollIntervalMs=options.screenshotPollIntervalMs??5000;await updateAppInfo(client,appId,store);await ensurePrice(client,appId,store);await ensureAvailability(client,appId,store.availableInNewTerritories??!0);const results=[];for(const version of versions){await updateVersion(client,version,store);const localizationId=await ensureVersionLocalization(client,version.id,store);await ensureReviewDetail(client,version.id,store);for(const displayType of displayTypesForPlatform(version.platform)){const paths=store.screenshots[displayType]??[];if(!paths.length)continue;const setId=await ensureScreenshotSet(client,localizationId,displayType);await syncScreenshotSet(client,setId,paths,cwd,timeoutMs,pollIntervalMs)}results.push({platform:version.platform,versionId:version.id,localizationId})}const shouldSubmit=options.submit??store.submitForReview??!1;return{appId,versions:results,reviewSubmissionIds:shouldSubmit?await submitVersions(client,appId,versions):[]}}export async function submitSafariAppStore(config,options){if(!config.safariBundleId)throw Error("[browser-extension] Safari App Store submission needs safariBundleId in config/extension.ts");const client=new AppStoreConnectClient(options),app=await client.findApp(config.safariBundleId);if(!app)throw Error("[browser-extension] the App Store Connect app record is missing");const selectedPlatforms=options.platforms??config.safariPlatforms??["macos"],availableVersions=await client.listAppStoreVersions(app.id),versions=[],versionStates=[];for(const platform of selectedPlatforms){const match=availableVersions.find((item)=>item.attributes.platform===applePlatform(platform)&&item.attributes.versionString===options.version);if(!match)throw Error(`[browser-extension] Safari ${platform} App Store version ${options.version} does not exist`);const build=await client.request(`/appStoreVersions/${match.id}/build`);versions.push({platform,id:match.id,buildId:build.data?.id});versionStates.push(match.attributes.appStoreState)}if((options.submit??config.safariAppStore?.submitForReview??!1)&&versionStates.every((state)=>!editableVersionStates.has(state))){const preparedVersions=await Promise.all(versions.map(async(version)=>{const localizations=await client.request(`/appStoreVersions/${version.id}/appStoreVersionLocalizations?limit=200`);return{platform:version.platform,versionId:version.id,localizationId:localizations.data[0]?.id??""}}));return{appId:app.id,versions:preparedVersions,reviewSubmissionIds:await submitVersions(client,app.id,versions)}}return await prepareSafariAppStoreSubmission(client,config,app.id,versions,options)}
package/dist/build.js CHANGED
@@ -1,119 +1,5 @@
1
- import { Glob } from "bun";
2
- import { cp, mkdir, rm } from "node:fs/promises";
3
- import { existsSync } from "node:fs";
4
- import { join, resolve } from "node:path";
5
- import { contentScriptOut, generateManifest } from "./manifest";
6
- import { sanitizeExtensionHtml } from "./sanitize";
7
- export function resolveOutdir(config, target, override) {
8
- if (override)
9
- return override;
10
- if (typeof config.outdir === "string")
11
- return config.outdir;
12
- if (config.outdir?.[target])
13
- return config.outdir[target];
14
- return target === "chrome" ? "dist" : `dist-${target}`;
15
- }
16
- const browserApiNamespaces = ["runtime", "tabs", "declarativeNetRequest", "storage", "action", "alarms", "scripting", "webNavigation", "cookies", "contextMenus", "notifications", "i18n", "downloads", "permissions", "windows", "bookmarks", "history", "search", "sidePanel", "omnibox", "idle", "management", "commands", "sessions", "topSites", "extension", "types", "devtools", "offscreen", "clipboard"], browserNamespacePattern = new RegExp(`\\bchrome\\.(?=(?:${browserApiNamespaces.join("|")})[?.])`, "g");
17
- export function rewriteBrowserNamespace(code) {
18
- const replacements = (code.match(browserNamespacePattern) ?? []).length;
19
- if (!replacements)
20
- return { code, replacements: 0 };
21
- return { code: code.replace(browserNamespacePattern, "browser."), replacements };
22
- }
23
- function normalizePage(page) {
24
- return typeof page === "string" ? { template: page } : page;
25
- }
26
- function pageEntries(pages) {
27
- if (!pages)
28
- return [];
29
- const out = [];
30
- if (pages.popup)
31
- out.push({ name: "popup", page: normalizePage(pages.popup) });
32
- if (pages.options)
33
- out.push({ name: "options", page: normalizePage(pages.options) });
34
- for (const [name, page] of Object.entries(pages.extra ?? {}))
35
- out.push({ name, page: normalizePage(page) });
36
- return out;
37
- }
38
- async function buildScript(entry, out, outdir, cwd, minify) {
39
- const result = await Bun.build({
40
- entrypoints: [resolve(cwd, entry)],
41
- target: "browser",
42
- format: "iife",
43
- splitting: !1,
44
- minify
45
- });
46
- if (!result.success)
47
- throw Error(`[browser-extension] failed to build ${entry}: ${result.logs.join(`
48
- `)}`);
49
- const output = result.outputs[0];
50
- if (!output)
51
- throw Error(`[browser-extension] build produced no output for ${entry}`);
52
- await Bun.write(join(outdir, out), await output.text());
53
- }
54
- export async function buildExtension(config, options) {
55
- const target = options.target ?? "chrome", cwd = options.cwd ?? process.cwd(), outdir = resolve(cwd, resolveOutdir(config, target, options.outdir)), minify = options.minify ?? !0;
56
- await rm(outdir, { recursive: !0, force: !0 });
57
- await mkdir(join(outdir, "rules"), { recursive: !0 });
58
- await mkdir(join(outdir, "icons"), { recursive: !0 });
59
- if (config.public) {
60
- const pub = resolve(cwd, config.public);
61
- if (existsSync(pub))
62
- await cp(pub, outdir, { recursive: !0 });
63
- }
64
- for (const [dest, src] of Object.entries(config.assets ?? {})) {
65
- const s = resolve(cwd, src);
66
- if (existsSync(s))
67
- await Bun.write(join(outdir, dest), Bun.file(s));
68
- }
69
- const pages = pageEntries(config.pages);
70
- await Promise.all(pages.filter((p) => p.page.script).map((p) => buildScript(p.page.script, `${p.name}.js`, outdir, cwd, minify)));
71
- if (pages.length) {
72
- const { default: stxPlugin } = await import("bun-plugin-stx"), resourcesPartials = resolve(cwd, "resources/partials"), legacyPartials = resolve(cwd, "partials"), partialsDir = existsSync(resourcesPartials) ? resourcesPartials : legacyPartials, result = await Bun.build({
73
- entrypoints: pages.map((p) => resolve(cwd, p.page.template)),
74
- outdir,
75
- minify,
76
- naming: { entry: "[name].html" },
77
- plugins: [stxPlugin({ partialsDir })]
78
- });
79
- if (!result.success)
80
- throw Error(`[browser-extension] failed to build pages: ${result.logs.join(`
81
- `)}`);
82
- for (const { name, page } of pages) {
83
- const file = join(outdir, `${name}.html`);
84
- if (!existsSync(file))
85
- continue;
86
- const own = page.script ? [`${name}.js`] : [];
87
- await Bun.write(file, sanitizeExtensionHtml(await Bun.file(file).text(), own));
88
- }
89
- for await (const chunk of new Glob("chunk-*.js").scan(outdir))
90
- await rm(join(outdir, chunk), { force: !0 });
91
- }
92
- const scripts = [];
93
- if (config.background)
94
- scripts.push({ entry: config.background, out: "background.js" });
95
- for (const cs of config.content ?? [])
96
- scripts.push({ entry: cs.entry, out: contentScriptOut(cs.entry, cs.out) });
97
- await Promise.all(scripts.map((s) => buildScript(s.entry, s.out, outdir, cwd, minify)));
98
- if (target === "safari")
99
- for await (const file of new Glob("*.js").scan(outdir)) {
100
- const path = join(outdir, file), { code, replacements } = rewriteBrowserNamespace(await Bun.file(path).text());
101
- if (replacements)
102
- await Bun.write(path, code);
103
- }
104
- for (const rule of config.rules ?? []) {
105
- if (!rule.source)
106
- continue;
107
- const mod = await import(resolve(cwd, rule.source)), build = mod.default ?? mod.buildRules ?? mod.rules ?? Object.values(mod).find((v) => typeof v === "function") ?? Object.values(mod).find((v) => Array.isArray(v)), data = typeof build === "function" ? await build() : build;
108
- await Bun.write(join(outdir, rule.path ?? `rules/${rule.id}.json`), `${JSON.stringify(data, null, 2)}
109
- `);
110
- }
111
- await Bun.write(join(outdir, "manifest.json"), `${JSON.stringify(generateManifest(config, { version: options.version, target }), null, 2)}
112
- `);
113
- await config.hooks?.postBuild?.({ config, target, outdir, version: options.version, cwd });
114
- return { outdir, target };
115
- }
116
- export async function buildAllTargets(config, options) {
117
- for (const target of config.targets ?? ["chrome", "firefox"])
118
- await buildExtension(config, { ...options, target });
119
- }
1
+ import{Glob}from"bun";import{cp,mkdir,rm}from"node:fs/promises";import{existsSync}from"node:fs";import{join,resolve}from"node:path";import{contentScriptOut,generateManifest}from"./manifest";import{sanitizeExtensionHtml}from"./sanitize";export function resolveOutdir(config,target,override){if(override)return override;if(typeof config.outdir==="string")return config.outdir;if(config.outdir?.[target])return config.outdir[target];return target==="chrome"?"dist":`dist-${target}`}const browserApiNamespaces=["runtime","tabs","declarativeNetRequest","storage","action","alarms","scripting","webNavigation","cookies","contextMenus","notifications","i18n","downloads","permissions","windows","bookmarks","history","search","sidePanel","omnibox","idle","management","commands","sessions","topSites","extension","types","devtools","offscreen","clipboard"],browserNamespacePattern=new RegExp(`\\bchrome\\.(?=(?:${browserApiNamespaces.join("|")})[?.])`,"g");export function rewriteBrowserNamespace(code){const replacements=(code.match(browserNamespacePattern)??[]).length;if(!replacements)return{code,replacements:0};return{code:code.replace(browserNamespacePattern,"browser."),replacements}}function normalizePage(page){return typeof page==="string"?{template:page}:page}function pageEntries(pages){if(!pages)return[];const out=[];if(pages.popup)out.push({name:"popup",page:normalizePage(pages.popup)});if(pages.options)out.push({name:"options",page:normalizePage(pages.options)});for(const[name,page]of Object.entries(pages.extra??{}))out.push({name,page:normalizePage(page)});return out}async function buildScript(entry,out,outdir,cwd,minify){const result=await Bun.build({entrypoints:[resolve(cwd,entry)],target:"browser",format:"iife",splitting:!1,minify});if(!result.success)throw Error(`[browser-extension] failed to build ${entry}: ${result.logs.join(`
2
+ `)}`);const output=result.outputs[0];if(!output)throw Error(`[browser-extension] build produced no output for ${entry}`);await Bun.write(join(outdir,out),await output.text())}export async function buildExtension(config,options){const target=options.target??"chrome",cwd=options.cwd??process.cwd(),outdir=resolve(cwd,resolveOutdir(config,target,options.outdir)),minify=options.minify??!0;await rm(outdir,{recursive:!0,force:!0});await mkdir(join(outdir,"rules"),{recursive:!0});await mkdir(join(outdir,"icons"),{recursive:!0});if(config.public){const pub=resolve(cwd,config.public);if(existsSync(pub))await cp(pub,outdir,{recursive:!0})}for(const[dest,src]of Object.entries(config.assets??{})){const s=resolve(cwd,src);if(existsSync(s))await Bun.write(join(outdir,dest),Bun.file(s))}const pages=pageEntries(config.pages);await Promise.all(pages.filter((p)=>p.page.script).map((p)=>buildScript(p.page.script,`${p.name}.js`,outdir,cwd,minify)));if(pages.length){const{default:stxPlugin}=await import("bun-plugin-stx"),resourcesPartials=resolve(cwd,"resources/partials"),legacyPartials=resolve(cwd,"partials"),partialsDir=existsSync(resourcesPartials)?resourcesPartials:legacyPartials,result=await Bun.build({entrypoints:pages.map((p)=>resolve(cwd,p.page.template)),outdir,minify,naming:{entry:"[name].html"},plugins:[stxPlugin({partialsDir})]});if(!result.success)throw Error(`[browser-extension] failed to build pages: ${result.logs.join(`
3
+ `)}`);for(const{name,page}of pages){const file=join(outdir,`${name}.html`);if(!existsSync(file))continue;const own=page.script?[`${name}.js`]:[];await Bun.write(file,sanitizeExtensionHtml(await Bun.file(file).text(),own))}for await(const chunk of new Glob("chunk-*.js").scan(outdir))await rm(join(outdir,chunk),{force:!0})}const scripts=[];if(config.background)scripts.push({entry:config.background,out:"background.js"});for(const cs of config.content??[])scripts.push({entry:cs.entry,out:contentScriptOut(cs.entry,cs.out)});await Promise.all(scripts.map((s)=>buildScript(s.entry,s.out,outdir,cwd,minify)));if(target==="safari")for await(const file of new Glob("*.js").scan(outdir)){const path=join(outdir,file),{code,replacements}=rewriteBrowserNamespace(await Bun.file(path).text());if(replacements)await Bun.write(path,code)}for(const rule of config.rules??[]){if(!rule.source)continue;const mod=await import(resolve(cwd,rule.source)),build=mod.default??mod.buildRules??mod.rules??Object.values(mod).find((v)=>typeof v==="function")??Object.values(mod).find((v)=>Array.isArray(v)),data=typeof build==="function"?await build():build;await Bun.write(join(outdir,rule.path??`rules/${rule.id}.json`),`${JSON.stringify(data,null,2)}
4
+ `)}await Bun.write(join(outdir,"manifest.json"),`${JSON.stringify(generateManifest(config,{version:options.version,target}),null,2)}
5
+ `);await config.hooks?.postBuild?.({config,target,outdir,version:options.version,cwd});return{outdir,target}}export async function buildAllTargets(config,options){for(const target of config.targets??["chrome","firefox"])await buildExtension(config,{...options,target})}