@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,165 +1 @@
1
- import { createPrivateKey, sign } from "node:crypto";
2
- import { existsSync, readFileSync } from "node:fs";
3
- import { resolve } from "node:path";
4
- import { packageExtension } from "./package";
5
- const chromeWebStoreScope = "https://www.googleapis.com/auth/chromewebstore", chromeWebStoreBaseUrl = "https://chromewebstore.googleapis.com";
6
- function base64urlJson(value) {
7
- return Buffer.from(JSON.stringify(value)).toString("base64url");
8
- }
9
- export function chromeWebStoreServiceAccountAssertion(serviceAccount, now = Math.floor(Date.now() / 1000)) {
10
- const tokenUri = serviceAccount.token_uri ?? "https://oauth2.googleapis.com/token", input = `${base64urlJson({ alg: "RS256", typ: "JWT" })}.${base64urlJson({
11
- iss: serviceAccount.client_email,
12
- scope: chromeWebStoreScope,
13
- aud: tokenUri,
14
- iat: now,
15
- exp: now + 3600
16
- })}`;
17
- let key;
18
- try {
19
- key = createPrivateKey(serviceAccount.private_key);
20
- } catch (error) {
21
- throw Error(`[browser-extension] Chrome Web Store service-account key could not be parsed: ${error instanceof Error ? error.message : String(error)}`);
22
- }
23
- return `${input}.${sign("sha256", Buffer.from(input), key).toString("base64url")}`;
24
- }
25
- async function resolveChromeAccessToken(options) {
26
- const supplied = options.accessToken ?? process.env.CHROME_WEB_STORE_ACCESS_TOKEN;
27
- if (supplied)
28
- return supplied;
29
- const configuredPath = options.serviceAccountPath ?? process.env.CHROME_WEB_STORE_SERVICE_ACCOUNT_PATH ?? process.env.GOOGLE_APPLICATION_CREDENTIALS;
30
- if (!configuredPath)
31
- throw Error("[browser-extension] missing Chrome Web Store credentials: CHROME_WEB_STORE_SERVICE_ACCOUNT_PATH or CHROME_WEB_STORE_ACCESS_TOKEN");
32
- const keyPath = resolve(configuredPath);
33
- if (!existsSync(keyPath))
34
- throw Error(`[browser-extension] Chrome Web Store service-account key not found: ${keyPath}`);
35
- let serviceAccount;
36
- try {
37
- serviceAccount = JSON.parse(readFileSync(keyPath, "utf8"));
38
- } catch (error) {
39
- throw Error(`[browser-extension] Chrome Web Store service-account JSON could not be parsed: ${error instanceof Error ? error.message : String(error)}`);
40
- }
41
- if (!serviceAccount.client_email || !serviceAccount.private_key)
42
- throw Error("[browser-extension] Chrome Web Store service-account JSON needs client_email and private_key");
43
- const tokenUri = serviceAccount.token_uri ?? "https://oauth2.googleapis.com/token", response = await (options.fetch ?? globalThis.fetch)(tokenUri, {
44
- method: "POST",
45
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
46
- body: new URLSearchParams({
47
- grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
48
- assertion: chromeWebStoreServiceAccountAssertion(serviceAccount, options.now?.())
49
- })
50
- }), body = await response.json().catch(() => ({}));
51
- if (!response.ok || !body.access_token)
52
- throw Error(`[browser-extension] Google OAuth token exchange failed (${response.status})${body.error_description ? `: ${body.error_description}` : ""}`);
53
- return body.access_token;
54
- }
55
- function storeItemName(config) {
56
- return `publishers/${encodeURIComponent(config.publisherId)}/items/${encodeURIComponent(config.itemId)}`;
57
- }
58
-
59
- export class ChromeWebStoreClient {
60
- options;
61
- baseUrl;
62
- accessToken;
63
- constructor(options = {}) {
64
- this.options = options;
65
- this.baseUrl = (options.baseUrl ?? chromeWebStoreBaseUrl).replace(/\/$/, "");
66
- }
67
- async request(path, init = {}) {
68
- this.accessToken ??= resolveChromeAccessToken(this.options);
69
- const response = await (this.options.fetch ?? globalThis.fetch)(`${this.baseUrl}${path}`, {
70
- ...init,
71
- headers: {
72
- Authorization: `Bearer ${await this.accessToken}`,
73
- ...init.headers
74
- }
75
- });
76
- if (!response.ok) {
77
- const body = await response.json().catch(() => ({})), detail = body.error?.message ?? body.message;
78
- throw Error(`[browser-extension] Chrome Web Store ${init.method ?? "GET"} ${path} failed (${response.status})${detail ? `: ${detail}` : ""}`);
79
- }
80
- return await response.json();
81
- }
82
- fetchStatus(config) {
83
- return this.request(`/v2/${storeItemName(config)}:fetchStatus`);
84
- }
85
- async upload(config, packagePath) {
86
- const file = Bun.file(resolve(packagePath));
87
- if (!await file.exists())
88
- throw Error(`[browser-extension] Chrome package not found: ${resolve(packagePath)}`);
89
- return await this.request(`/upload/v2/${storeItemName(config)}:upload`, {
90
- method: "POST",
91
- headers: { "Content-Type": "application/zip" },
92
- body: new Uint8Array(await file.arrayBuffer())
93
- });
94
- }
95
- publish(config, options = {}) {
96
- if (config.deployPercentage !== void 0 && (!Number.isInteger(config.deployPercentage) || config.deployPercentage < 0 || config.deployPercentage > 100))
97
- throw Error("[browser-extension] chromeWebStore.deployPercentage must be an integer from 0 to 100");
98
- const body = {
99
- publishType: config.publishType ?? "DEFAULT_PUBLISH",
100
- skipReview: config.skipReview ?? !1,
101
- blockOnWarnings: options.blockOnWarnings ?? !0
102
- };
103
- if (config.deployPercentage !== void 0)
104
- body.deployInfos = [{ deployPercentage: config.deployPercentage }];
105
- return this.request(`/v2/${storeItemName(config)}:publish`, {
106
- method: "POST",
107
- headers: { "Content-Type": "application/json" },
108
- body: JSON.stringify(body)
109
- });
110
- }
111
- async waitForUpload(config, maxAttempts = 30) {
112
- for (let attempt = 0;attempt < maxAttempts; attempt += 1) {
113
- const status = await this.fetchStatus(config);
114
- if (status.lastAsyncUploadState !== "IN_PROGRESS") {
115
- if (status.lastAsyncUploadState === "FAILED")
116
- throw Error("[browser-extension] Chrome Web Store package processing failed");
117
- return status;
118
- }
119
- await (this.options.wait ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))))(2000);
120
- }
121
- throw Error("[browser-extension] Chrome Web Store package processing timed out");
122
- }
123
- }
124
- function pendingReview(status) {
125
- if (status.submittedItemRevisionStatus?.state !== "PENDING_REVIEW")
126
- return;
127
- const submittedVersion = status.submittedItemRevisionStatus.distributionChannels?.[0]?.crxVersion;
128
- return {
129
- state: status.submittedItemRevisionStatus.state,
130
- submittedVersion,
131
- reason: submittedVersion ? `Chrome Web Store version ${submittedVersion} is already pending review` : "A Chrome Web Store revision is already pending review"
132
- };
133
- }
134
- function publishedVersion(status, version) {
135
- const published = status.publishedItemRevisionStatus;
136
- if (!published || !["PUBLISHED", "PUBLISHED_TO_TESTERS"].includes(published.state))
137
- return;
138
- if (!published.distributionChannels?.some((channel) => channel.crxVersion === version))
139
- return;
140
- return {
141
- state: published.state,
142
- version,
143
- reason: `Chrome Web Store version ${version} is already ${published.state.toLowerCase().replaceAll("_", " ")}`
144
- };
145
- }
146
- export async function publishChromeExtension(config, options) {
147
- if (!config.chromeWebStore)
148
- throw Error("[browser-extension] Chrome publishing needs chromeWebStore.publisherId and chromeWebStore.itemId in config/extension.ts");
149
- const cwd = options.cwd ?? process.cwd(), packagePath = options.packagePath ?? await packageExtension(config, { target: "chrome", version: options.version, cwd }), client = new ChromeWebStoreClient(options), status = await client.fetchStatus(config.chromeWebStore), alreadyPublished = publishedVersion(status, options.version);
150
- if (alreadyPublished)
151
- return { packagePath, alreadyPublished };
152
- const deferred = pendingReview(status);
153
- if (deferred)
154
- return { packagePath, deferred };
155
- const upload = await client.upload(config.chromeWebStore, packagePath);
156
- if (upload.uploadState === "IN_PROGRESS")
157
- await client.waitForUpload(config.chromeWebStore);
158
- else if (upload.uploadState !== "SUCCEEDED")
159
- throw Error(`[browser-extension] Chrome Web Store upload failed with state ${upload.uploadState}`);
160
- return {
161
- packagePath,
162
- upload,
163
- publish: options.uploadOnly ? void 0 : await client.publish(config.chromeWebStore, { blockOnWarnings: options.blockOnWarnings })
164
- };
165
- }
1
+ import{createPrivateKey,sign}from"node:crypto";import{existsSync,readFileSync}from"node:fs";import{resolve}from"node:path";import{packageExtension}from"./package";const chromeWebStoreScope="https://www.googleapis.com/auth/chromewebstore",chromeWebStoreBaseUrl="https://chromewebstore.googleapis.com";function base64urlJson(value){return Buffer.from(JSON.stringify(value)).toString("base64url")}export function chromeWebStoreServiceAccountAssertion(serviceAccount,now=Math.floor(Date.now()/1000)){const tokenUri=serviceAccount.token_uri??"https://oauth2.googleapis.com/token",input=`${base64urlJson({alg:"RS256",typ:"JWT"})}.${base64urlJson({iss:serviceAccount.client_email,scope:chromeWebStoreScope,aud:tokenUri,iat:now,exp:now+3600})}`;let key;try{key=createPrivateKey(serviceAccount.private_key)}catch(error){throw Error(`[browser-extension] Chrome Web Store service-account key could not be parsed: ${error instanceof Error?error.message:String(error)}`)}return`${input}.${sign("sha256",Buffer.from(input),key).toString("base64url")}`}async function resolveChromeAccessToken(options){const supplied=options.accessToken??process.env.CHROME_WEB_STORE_ACCESS_TOKEN;if(supplied)return supplied;const configuredPath=options.serviceAccountPath??process.env.CHROME_WEB_STORE_SERVICE_ACCOUNT_PATH??process.env.GOOGLE_APPLICATION_CREDENTIALS;if(!configuredPath)throw Error("[browser-extension] missing Chrome Web Store credentials: CHROME_WEB_STORE_SERVICE_ACCOUNT_PATH or CHROME_WEB_STORE_ACCESS_TOKEN");const keyPath=resolve(configuredPath);if(!existsSync(keyPath))throw Error(`[browser-extension] Chrome Web Store service-account key not found: ${keyPath}`);let serviceAccount;try{serviceAccount=JSON.parse(readFileSync(keyPath,"utf8"))}catch(error){throw Error(`[browser-extension] Chrome Web Store service-account JSON could not be parsed: ${error instanceof Error?error.message:String(error)}`)}if(!serviceAccount.client_email||!serviceAccount.private_key)throw Error("[browser-extension] Chrome Web Store service-account JSON needs client_email and private_key");const tokenUri=serviceAccount.token_uri??"https://oauth2.googleapis.com/token",response=await(options.fetch??globalThis.fetch)(tokenUri,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"urn:ietf:params:oauth:grant-type:jwt-bearer",assertion:chromeWebStoreServiceAccountAssertion(serviceAccount,options.now?.())})}),body=await response.json().catch(()=>({}));if(!response.ok||!body.access_token)throw Error(`[browser-extension] Google OAuth token exchange failed (${response.status})${body.error_description?`: ${body.error_description}`:""}`);return body.access_token}function storeItemName(config){return`publishers/${encodeURIComponent(config.publisherId)}/items/${encodeURIComponent(config.itemId)}`}export class ChromeWebStoreClient{options;baseUrl;accessToken;constructor(options={}){this.options=options;this.baseUrl=(options.baseUrl??chromeWebStoreBaseUrl).replace(/\/$/,"")}async request(path,init={}){this.accessToken??=resolveChromeAccessToken(this.options);const response=await(this.options.fetch??globalThis.fetch)(`${this.baseUrl}${path}`,{...init,headers:{Authorization:`Bearer ${await this.accessToken}`,...init.headers}});if(!response.ok){const body=await response.json().catch(()=>({})),detail=body.error?.message??body.message;throw Error(`[browser-extension] Chrome Web Store ${init.method??"GET"} ${path} failed (${response.status})${detail?`: ${detail}`:""}`)}return await response.json()}fetchStatus(config){return this.request(`/v2/${storeItemName(config)}:fetchStatus`)}async upload(config,packagePath){const file=Bun.file(resolve(packagePath));if(!await file.exists())throw Error(`[browser-extension] Chrome package not found: ${resolve(packagePath)}`);return await this.request(`/upload/v2/${storeItemName(config)}:upload`,{method:"POST",headers:{"Content-Type":"application/zip"},body:new Uint8Array(await file.arrayBuffer())})}publish(config,options={}){if(config.deployPercentage!==void 0&&(!Number.isInteger(config.deployPercentage)||config.deployPercentage<0||config.deployPercentage>100))throw Error("[browser-extension] chromeWebStore.deployPercentage must be an integer from 0 to 100");const body={publishType:config.publishType??"DEFAULT_PUBLISH",skipReview:config.skipReview??!1,blockOnWarnings:options.blockOnWarnings??!0};if(config.deployPercentage!==void 0)body.deployInfos=[{deployPercentage:config.deployPercentage}];return this.request(`/v2/${storeItemName(config)}:publish`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)})}async waitForUpload(config,maxAttempts=30){for(let attempt=0;attempt<maxAttempts;attempt+=1){const status=await this.fetchStatus(config);if(status.lastAsyncUploadState!=="IN_PROGRESS"){if(status.lastAsyncUploadState==="FAILED")throw Error("[browser-extension] Chrome Web Store package processing failed");return status}await(this.options.wait??((milliseconds)=>new Promise((resolve)=>setTimeout(resolve,milliseconds))))(2000)}throw Error("[browser-extension] Chrome Web Store package processing timed out")}}function pendingReview(status){if(status.submittedItemRevisionStatus?.state!=="PENDING_REVIEW")return;const submittedVersion=status.submittedItemRevisionStatus.distributionChannels?.[0]?.crxVersion;return{state:status.submittedItemRevisionStatus.state,submittedVersion,reason:submittedVersion?`Chrome Web Store version ${submittedVersion} is already pending review`:"A Chrome Web Store revision is already pending review"}}function publishedVersion(status,version){const published=status.publishedItemRevisionStatus;if(!published||!["PUBLISHED","PUBLISHED_TO_TESTERS"].includes(published.state))return;if(!published.distributionChannels?.some((channel)=>channel.crxVersion===version))return;return{state:published.state,version,reason:`Chrome Web Store version ${version} is already ${published.state.toLowerCase().replaceAll("_"," ")}`}}export async function publishChromeExtension(config,options){if(!config.chromeWebStore)throw Error("[browser-extension] Chrome publishing needs chromeWebStore.publisherId and chromeWebStore.itemId in config/extension.ts");const cwd=options.cwd??process.cwd(),packagePath=options.packagePath??await packageExtension(config,{target:"chrome",version:options.version,cwd}),client=new ChromeWebStoreClient(options),status=await client.fetchStatus(config.chromeWebStore),alreadyPublished=publishedVersion(status,options.version);if(alreadyPublished)return{packagePath,alreadyPublished};const deferred=pendingReview(status);if(deferred)return{packagePath,deferred};const upload=await client.upload(config.chromeWebStore,packagePath);if(upload.uploadState==="IN_PROGRESS")await client.waitForUpload(config.chromeWebStore);else if(upload.uploadState!=="SUCCEEDED")throw Error(`[browser-extension] Chrome Web Store upload failed with state ${upload.uploadState}`);return{packagePath,upload,publish:options.uploadOnly?void 0:await client.publish(config.chromeWebStore,{blockOnWarnings:options.blockOnWarnings})}}
package/dist/config.js CHANGED
@@ -1,16 +1 @@
1
- import { existsSync } from "node:fs";
2
- import { resolve } from "node:path";
3
- export function defineExtension(config) {
4
- return config;
5
- }
6
- export async function loadExtensionConfig(cwd = process.cwd()) {
7
- for (const rel of ["config/extension.ts", "extension.config.ts"]) {
8
- const path = resolve(cwd, rel);
9
- if (!existsSync(path))
10
- continue;
11
- const mod = await import(path), config = mod.default ?? mod.extension ?? mod.config;
12
- if (config)
13
- return config;
14
- }
15
- return null;
16
- }
1
+ import{existsSync}from"node:fs";import{resolve}from"node:path";export function defineExtension(config){return config}export async function loadExtensionConfig(cwd=process.cwd()){for(const rel of["config/extension.ts","extension.config.ts"]){const path=resolve(cwd,rel);if(!existsSync(path))continue;const mod=await import(path),config=mod.default??mod.extension??mod.config;if(config)return config}return null}
@@ -1,106 +1,2 @@
1
- import { existsSync, readFileSync, readdirSync } from "node:fs";
2
- import { mkdir, mkdtemp, rm } from "node:fs/promises";
3
- import { createRequire } from "node:module";
4
- import { tmpdir } from "node:os";
5
- import { join, resolve } from "node:path";
6
- import { buildExtension, resolveOutdir } from "./build";
7
- export function firefoxSignArgs(options) {
8
- return [
9
- options.executable,
10
- "sign",
11
- "--source-dir",
12
- options.sourceDir,
13
- "--artifacts-dir",
14
- options.artifactsDir,
15
- "--channel",
16
- options.channel,
17
- "--timeout",
18
- String(options.timeout),
19
- "--approval-timeout",
20
- String(options.approvalTimeout),
21
- "--no-input"
22
- ];
23
- }
24
- function resolveFirefoxAuth(options) {
25
- const issuer = options.issuer ?? process.env.AMO_JWT_ISSUER ?? process.env.WEB_EXT_API_KEY, secret = options.secret ?? process.env.AMO_JWT_SECRET ?? process.env.WEB_EXT_API_SECRET, missing = [!issuer && "AMO_JWT_ISSUER", !secret && "AMO_JWT_SECRET"].filter(Boolean);
26
- if (missing.length)
27
- throw Error(`[browser-extension] missing Firefox Add-ons credentials: ${missing.join(", ")}`);
28
- return { issuer, secret };
29
- }
30
- export function firefoxListingMetadata(config, store) {
31
- if (!store.license && !store.categories?.length)
32
- return;
33
- if (!store.license || !store.categories?.length)
34
- throw Error("[browser-extension] a new Firefox listing needs both firefoxAddons.license and firefoxAddons.categories");
35
- return {
36
- version: { license: store.license },
37
- categories: { firefox: store.categories },
38
- summary: { "en-US": config.description },
39
- ...store.homepage ? { homepage: { "en-US": store.homepage } } : {},
40
- ...store.supportEmail ? { support_email: { "en-US": store.supportEmail } } : {},
41
- requires_payment: store.requiresPayment ?? !1
42
- };
43
- }
44
- export function resolveWebExtExecutable(which = Bun.which, resolvePackage = (specifier) => createRequire(import.meta.url).resolve(specifier)) {
45
- const fromPath = which("web-ext");
46
- if (fromPath)
47
- return fromPath;
48
- try {
49
- const packagePath = resolvePackage("web-ext/package.json"), packageJson = JSON.parse(readFileSync(packagePath, "utf8")), relativeBin = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.["web-ext"];
50
- if (!relativeBin)
51
- return;
52
- const executable = resolve(packagePath, "..", relativeBin);
53
- return existsSync(executable) ? executable : void 0;
54
- } catch {
55
- return;
56
- }
57
- }
58
- export async function publishFirefoxExtension(config, options) {
59
- if (!config.geckoId)
60
- throw Error("[browser-extension] Firefox publishing needs geckoId in config/extension.ts");
61
- const store = config.firefoxAddons ?? {}, auth = resolveFirefoxAuth(options), cwd = options.cwd ?? process.cwd();
62
- if (options.build !== !1)
63
- await buildExtension(config, { target: "firefox", version: options.version, cwd });
64
- const sourceDir = resolve(cwd, resolveOutdir(config, "firefox")), artifactsDir = resolve(cwd, store.artifactsDir ?? "web-ext-artifacts"), executable = resolveWebExtExecutable();
65
- if (!executable)
66
- throw Error("[browser-extension] Firefox publishing needs web-ext, an optional peer dependency. Install it with `bun add -d web-ext`.");
67
- await mkdir(artifactsDir, { recursive: !0 });
68
- const before = new Set(existsSync(artifactsDir) ? readdirSync(artifactsDir) : []), tempDir = await mkdtemp(join(tmpdir(), "stacks-firefox-publish-"));
69
- try {
70
- const args = firefoxSignArgs({
71
- executable,
72
- sourceDir,
73
- artifactsDir,
74
- channel: store.channel ?? "listed",
75
- timeout: options.timeout ?? 300000,
76
- approvalTimeout: options.approvalTimeout ?? 0
77
- }), metadata = firefoxListingMetadata(config, store);
78
- if (metadata) {
79
- const metadataPath = join(tempDir, "amo-metadata.json");
80
- await Bun.write(metadataPath, `${JSON.stringify(metadata, null, 2)}
81
- `);
82
- args.push("--amo-metadata", metadataPath);
83
- }
84
- if (options.sourceCodePath)
85
- args.push("--upload-source-code", resolve(cwd, options.sourceCodePath));
86
- const exitCode = await Bun.spawn(args, {
87
- cwd,
88
- env: {
89
- ...process.env,
90
- WEB_EXT_API_KEY: auth.issuer,
91
- WEB_EXT_API_SECRET: auth.secret
92
- },
93
- stdout: "inherit",
94
- stderr: "inherit"
95
- }).exited;
96
- if (exitCode !== 0)
97
- throw Error(`[browser-extension] Firefox Add-ons submission failed (${exitCode})`);
98
- } finally {
99
- await rm(tempDir, { recursive: !0, force: !0 });
100
- }
101
- return {
102
- artifactsDir,
103
- artifacts: readdirSync(artifactsDir).filter((file) => !before.has(file)),
104
- channel: store.channel ?? "listed"
105
- };
106
- }
1
+ import{existsSync,readFileSync,readdirSync}from"node:fs";import{mkdir,mkdtemp,rm}from"node:fs/promises";import{createRequire}from"node:module";import{tmpdir}from"node:os";import{join,resolve}from"node:path";import{buildExtension,resolveOutdir}from"./build";export function firefoxSignArgs(options){return[options.executable,"sign","--source-dir",options.sourceDir,"--artifacts-dir",options.artifactsDir,"--channel",options.channel,"--timeout",String(options.timeout),"--approval-timeout",String(options.approvalTimeout),"--no-input"]}function resolveFirefoxAuth(options){const issuer=options.issuer??process.env.AMO_JWT_ISSUER??process.env.WEB_EXT_API_KEY,secret=options.secret??process.env.AMO_JWT_SECRET??process.env.WEB_EXT_API_SECRET,missing=[!issuer&&"AMO_JWT_ISSUER",!secret&&"AMO_JWT_SECRET"].filter(Boolean);if(missing.length)throw Error(`[browser-extension] missing Firefox Add-ons credentials: ${missing.join(", ")}`);return{issuer,secret}}export function firefoxListingMetadata(config,store){if(!store.license&&!store.categories?.length)return;if(!store.license||!store.categories?.length)throw Error("[browser-extension] a new Firefox listing needs both firefoxAddons.license and firefoxAddons.categories");return{version:{license:store.license},categories:{firefox:store.categories},summary:{"en-US":config.description},...store.homepage?{homepage:{"en-US":store.homepage}}:{},...store.supportEmail?{support_email:{"en-US":store.supportEmail}}:{},requires_payment:store.requiresPayment??!1}}export function resolveWebExtExecutable(which=Bun.which,resolvePackage=(specifier)=>createRequire(import.meta.url).resolve(specifier)){const fromPath=which("web-ext");if(fromPath)return fromPath;try{const packagePath=resolvePackage("web-ext/package.json"),packageJson=JSON.parse(readFileSync(packagePath,"utf8")),relativeBin=typeof packageJson.bin==="string"?packageJson.bin:packageJson.bin?.["web-ext"];if(!relativeBin)return;const executable=resolve(packagePath,"..",relativeBin);return existsSync(executable)?executable:void 0}catch{return}}export async function publishFirefoxExtension(config,options){if(!config.geckoId)throw Error("[browser-extension] Firefox publishing needs geckoId in config/extension.ts");const store=config.firefoxAddons??{},auth=resolveFirefoxAuth(options),cwd=options.cwd??process.cwd();if(options.build!==!1)await buildExtension(config,{target:"firefox",version:options.version,cwd});const sourceDir=resolve(cwd,resolveOutdir(config,"firefox")),artifactsDir=resolve(cwd,store.artifactsDir??"web-ext-artifacts"),executable=resolveWebExtExecutable();if(!executable)throw Error("[browser-extension] Firefox publishing needs web-ext, an optional peer dependency. Install it with `bun add -d web-ext`.");await mkdir(artifactsDir,{recursive:!0});const before=new Set(existsSync(artifactsDir)?readdirSync(artifactsDir):[]),tempDir=await mkdtemp(join(tmpdir(),"stacks-firefox-publish-"));try{const args=firefoxSignArgs({executable,sourceDir,artifactsDir,channel:store.channel??"listed",timeout:options.timeout??300000,approvalTimeout:options.approvalTimeout??0}),metadata=firefoxListingMetadata(config,store);if(metadata){const metadataPath=join(tempDir,"amo-metadata.json");await Bun.write(metadataPath,`${JSON.stringify(metadata,null,2)}
2
+ `);args.push("--amo-metadata",metadataPath)}if(options.sourceCodePath)args.push("--upload-source-code",resolve(cwd,options.sourceCodePath));const exitCode=await Bun.spawn(args,{cwd,env:{...process.env,WEB_EXT_API_KEY:auth.issuer,WEB_EXT_API_SECRET:auth.secret},stdout:"inherit",stderr:"inherit"}).exited;if(exitCode!==0)throw Error(`[browser-extension] Firefox Add-ons submission failed (${exitCode})`)}finally{await rm(tempDir,{recursive:!0,force:!0})}return{artifactsDir,artifacts:readdirSync(artifactsDir).filter((file)=>!before.has(file)),channel:store.channel??"listed"}}
@@ -1,123 +1 @@
1
- import { createHmac, randomUUID } from "node:crypto";
2
- import { readFile } from "node:fs/promises";
3
- import { basename, isAbsolute, resolve } from "node:path";
4
- import process from "node:process";
5
- const AMO_API = "https://addons.mozilla.org/api/v5";
6
- export function imageSize(bytes) {
7
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
8
- if (bytes.length > 24 && view.getUint32(0) === 2303741511)
9
- return { width: view.getUint32(16), height: view.getUint32(20) };
10
- if (bytes.length > 4 && view.getUint16(0) === 65496) {
11
- let offset = 2;
12
- while (offset + 9 < bytes.length) {
13
- if (view.getUint8(offset) !== 255) {
14
- offset++;
15
- continue;
16
- }
17
- const marker = view.getUint8(offset + 1);
18
- if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204)
19
- return { height: view.getUint16(offset + 5), width: view.getUint16(offset + 7) };
20
- offset += 2 + view.getUint16(offset + 2);
21
- }
22
- }
23
- return;
24
- }
25
- export function amoToken(issuer, secret, now = Date.now()) {
26
- const base64url = (value) => Buffer.from(value).toString("base64url"), issuedAt = Math.floor(now / 1000), header = base64url(JSON.stringify({ alg: "HS256", typ: "JWT" })), payload = base64url(JSON.stringify({
27
- iss: issuer,
28
- jti: randomUUID(),
29
- iat: issuedAt,
30
- exp: issuedAt + 240
31
- })), signature = createHmac("sha256", secret).update(`${header}.${payload}`).digest("base64url");
32
- return `${header}.${payload}.${signature}`;
33
- }
34
- function resolveAuth(options) {
35
- const issuer = options.issuer ?? process.env.AMO_JWT_ISSUER ?? process.env.WEB_EXT_API_KEY, secret = options.secret ?? process.env.AMO_JWT_SECRET ?? process.env.WEB_EXT_API_SECRET, missing = [!issuer && "AMO_JWT_ISSUER", !secret && "AMO_JWT_SECRET"].filter(Boolean);
36
- if (missing.length)
37
- throw Error(`[browser-extension] missing Firefox Add-ons credentials: ${missing.join(", ")}`);
38
- return { issuer, secret };
39
- }
40
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
41
- function retryAfterMs(response, body) {
42
- const header = Number(response.headers.get("retry-after"));
43
- if (Number.isFinite(header) && header > 0)
44
- return header * 1000;
45
- const stated = /available in (\d+) seconds?/i.exec(body);
46
- return stated ? Number(stated[1]) * 1000 : void 0;
47
- }
48
- async function amo(path, auth, init = {}, attempt = 0) {
49
- const response = await fetch(`${AMO_API}${path}`, {
50
- ...init,
51
- headers: { Authorization: `JWT ${amoToken(auth.issuer, auth.secret)}`, ...init.headers }
52
- });
53
- if (response.ok || response.status === 404)
54
- return response;
55
- const detail = await response.text().catch(() => "");
56
- if (response.status === 429 && attempt < 4) {
57
- await sleep((retryAfterMs(response, detail) ?? 60000) + 1000);
58
- return amo(path, auth, init, attempt + 1);
59
- }
60
- throw Error(`[browser-extension] AMO ${init.method ?? "GET"} ${path} failed (${response.status})${detail ? `: ${detail.slice(0, 300)}` : ""}`);
61
- }
62
- export async function listFirefoxPreviews(addonId, auth) {
63
- const response = await amo(`/addons/addon/${encodeURIComponent(addonId)}/`, auth);
64
- if (response.status === 404)
65
- return [];
66
- return ((await response.json()).previews ?? []).map((preview, index) => ({
67
- id: preview.id,
68
- position: preview.position ?? index,
69
- size: preview.image_size ?? [0, 0],
70
- caption: typeof preview.caption === "string" ? preview.caption : preview.caption?.["en-US"]
71
- }));
72
- }
73
- export async function syncFirefoxPreviews(config, options = {}) {
74
- const store = config.firefoxAddons ?? {}, screenshots = store.screenshots ?? [];
75
- if (!screenshots.length)
76
- return { unchanged: !0, uploaded: [], removed: [] };
77
- if (!config.geckoId)
78
- throw Error("[browser-extension] Firefox previews need geckoId in config/extension.ts");
79
- const cwd = options.cwd ?? process.cwd(), files = await Promise.all(screenshots.map(async (path) => {
80
- const file = isAbsolute(path) ? path : resolve(cwd, path), bytes = new Uint8Array(await readFile(file).catch(() => {
81
- throw Error(`[browser-extension] Firefox screenshot not found: ${path}`);
82
- })), size = imageSize(bytes);
83
- return { path: file, bytes, width: size?.width ?? 0, height: size?.height ?? 0 };
84
- })), auth = resolveAuth(options), existing = await listFirefoxPreviews(config.geckoId, auth), ordered = existing.slice().sort((a, b) => a.position - b.position), imagesMatch = files.every((file) => file.width > 0 && file.height > 0) && ordered.length === files.length && ordered.every((preview, index) => preview.size[0] === files[index].width && preview.size[1] === files[index].height), captionFor = (index) => store.screenshotCaptions?.[index] ?? "", drifted = imagesMatch ? ordered.map((preview, index) => ({ preview, caption: captionFor(index) })).filter((entry) => (entry.preview.caption ?? "") !== entry.caption) : [];
85
- if (imagesMatch && !drifted.length)
86
- return { unchanged: !0, uploaded: [], removed: [] };
87
- if (imagesMatch) {
88
- if (options.dryRun)
89
- return { unchanged: !1, uploaded: [], removed: [] };
90
- for (const { preview, caption } of drifted)
91
- await amo(`/addons/addon/${encodeURIComponent(config.geckoId)}/previews/${preview.id}/`, auth, {
92
- method: "PATCH",
93
- headers: { "Content-Type": "application/json" },
94
- body: JSON.stringify({ caption: caption ? { "en-US": caption } : null })
95
- });
96
- return { unchanged: !1, uploaded: ordered, removed: [] };
97
- }
98
- if (options.dryRun)
99
- return { unchanged: !1, uploaded: [], removed: existing.map((preview) => preview.id) };
100
- const uploaded = [];
101
- for (const [index, file] of files.entries()) {
102
- const form = new FormData;
103
- form.append("image", new Blob([file.bytes], { type: "image/png" }), basename(file.path));
104
- form.append("position", String(index));
105
- const created = await (await amo(`/addons/addon/${encodeURIComponent(config.geckoId)}/previews/`, auth, {
106
- method: "POST",
107
- body: form
108
- })).json(), caption = store.screenshotCaptions?.[index];
109
- if (caption)
110
- await amo(`/addons/addon/${encodeURIComponent(config.geckoId)}/previews/${created.id}/`, auth, {
111
- method: "PATCH",
112
- headers: { "Content-Type": "application/json" },
113
- body: JSON.stringify({ caption: { "en-US": caption } })
114
- });
115
- uploaded.push({ id: created.id, position: index, size: [file.width, file.height] });
116
- }
117
- const removed = [];
118
- for (const preview of existing) {
119
- await amo(`/addons/addon/${encodeURIComponent(config.geckoId)}/previews/${preview.id}/`, auth, { method: "DELETE" });
120
- removed.push(preview.id);
121
- }
122
- return { unchanged: !1, uploaded, removed };
123
- }
1
+ import{createHmac,randomUUID}from"node:crypto";import{readFile}from"node:fs/promises";import{basename,isAbsolute,resolve}from"node:path";import process from"node:process";const AMO_API="https://addons.mozilla.org/api/v5";export function imageSize(bytes){const view=new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength);if(bytes.length>24&&view.getUint32(0)===2303741511)return{width:view.getUint32(16),height:view.getUint32(20)};if(bytes.length>4&&view.getUint16(0)===65496){let offset=2;while(offset+9<bytes.length){if(view.getUint8(offset)!==255){offset++;continue}const marker=view.getUint8(offset+1);if(marker>=192&&marker<=207&&marker!==196&&marker!==200&&marker!==204)return{height:view.getUint16(offset+5),width:view.getUint16(offset+7)};offset+=2+view.getUint16(offset+2)}}return}export function amoToken(issuer,secret,now=Date.now()){const base64url=(value)=>Buffer.from(value).toString("base64url"),issuedAt=Math.floor(now/1000),header=base64url(JSON.stringify({alg:"HS256",typ:"JWT"})),payload=base64url(JSON.stringify({iss:issuer,jti:randomUUID(),iat:issuedAt,exp:issuedAt+240})),signature=createHmac("sha256",secret).update(`${header}.${payload}`).digest("base64url");return`${header}.${payload}.${signature}`}function resolveAuth(options){const issuer=options.issuer??process.env.AMO_JWT_ISSUER??process.env.WEB_EXT_API_KEY,secret=options.secret??process.env.AMO_JWT_SECRET??process.env.WEB_EXT_API_SECRET,missing=[!issuer&&"AMO_JWT_ISSUER",!secret&&"AMO_JWT_SECRET"].filter(Boolean);if(missing.length)throw Error(`[browser-extension] missing Firefox Add-ons credentials: ${missing.join(", ")}`);return{issuer,secret}}const sleep=(ms)=>new Promise((resolve)=>setTimeout(resolve,ms));function retryAfterMs(response,body){const header=Number(response.headers.get("retry-after"));if(Number.isFinite(header)&&header>0)return header*1000;const stated=/available in (\d+) seconds?/i.exec(body);return stated?Number(stated[1])*1000:void 0}async function amo(path,auth,init={},attempt=0){const response=await fetch(`${AMO_API}${path}`,{...init,headers:{Authorization:`JWT ${amoToken(auth.issuer,auth.secret)}`,...init.headers}});if(response.ok||response.status===404)return response;const detail=await response.text().catch(()=>"");if(response.status===429&&attempt<4){await sleep((retryAfterMs(response,detail)??60000)+1000);return amo(path,auth,init,attempt+1)}throw Error(`[browser-extension] AMO ${init.method??"GET"} ${path} failed (${response.status})${detail?`: ${detail.slice(0,300)}`:""}`)}export async function listFirefoxPreviews(addonId,auth){const response=await amo(`/addons/addon/${encodeURIComponent(addonId)}/`,auth);if(response.status===404)return[];return((await response.json()).previews??[]).map((preview,index)=>({id:preview.id,position:preview.position??index,size:preview.image_size??[0,0],caption:typeof preview.caption==="string"?preview.caption:preview.caption?.["en-US"]}))}export async function syncFirefoxPreviews(config,options={}){const store=config.firefoxAddons??{},screenshots=store.screenshots??[];if(!screenshots.length)return{unchanged:!0,uploaded:[],removed:[]};if(!config.geckoId)throw Error("[browser-extension] Firefox previews need geckoId in config/extension.ts");const cwd=options.cwd??process.cwd(),files=await Promise.all(screenshots.map(async(path)=>{const file=isAbsolute(path)?path:resolve(cwd,path),bytes=new Uint8Array(await readFile(file).catch(()=>{throw Error(`[browser-extension] Firefox screenshot not found: ${path}`)})),size=imageSize(bytes);return{path:file,bytes,width:size?.width??0,height:size?.height??0}})),auth=resolveAuth(options),existing=await listFirefoxPreviews(config.geckoId,auth),ordered=existing.slice().sort((a,b)=>a.position-b.position),imagesMatch=files.every((file)=>file.width>0&&file.height>0)&&ordered.length===files.length&&ordered.every((preview,index)=>preview.size[0]===files[index].width&&preview.size[1]===files[index].height),captionFor=(index)=>store.screenshotCaptions?.[index]??"",drifted=imagesMatch?ordered.map((preview,index)=>({preview,caption:captionFor(index)})).filter((entry)=>(entry.preview.caption??"")!==entry.caption):[];if(imagesMatch&&!drifted.length)return{unchanged:!0,uploaded:[],removed:[]};if(imagesMatch){if(options.dryRun)return{unchanged:!1,uploaded:[],removed:[]};for(const{preview,caption}of drifted)await amo(`/addons/addon/${encodeURIComponent(config.geckoId)}/previews/${preview.id}/`,auth,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({caption:caption?{"en-US":caption}:null})});return{unchanged:!1,uploaded:ordered,removed:[]}}if(options.dryRun)return{unchanged:!1,uploaded:[],removed:existing.map((preview)=>preview.id)};const uploaded=[];for(const[index,file]of files.entries()){const form=new FormData;form.append("image",new Blob([file.bytes],{type:"image/png"}),basename(file.path));form.append("position",String(index));const created=await(await amo(`/addons/addon/${encodeURIComponent(config.geckoId)}/previews/`,auth,{method:"POST",body:form})).json(),caption=store.screenshotCaptions?.[index];if(caption)await amo(`/addons/addon/${encodeURIComponent(config.geckoId)}/previews/${created.id}/`,auth,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({caption:{"en-US":caption}})});uploaded.push({id:created.id,position:index,size:[file.width,file.height]})}const removed=[];for(const preview of existing){await amo(`/addons/addon/${encodeURIComponent(config.geckoId)}/previews/${preview.id}/`,auth,{method:"DELETE"});removed.push(preview.id)}return{unchanged:!1,uploaded,removed}}
package/dist/index.js CHANGED
@@ -1,13 +1 @@
1
- export * from "./build";
2
- export * from "./app-store-connect";
3
- export * from "./app-store-submission";
4
- export * from "./chrome-web-store";
5
- export * from "./config";
6
- export * from "./firefox-addons";
7
- export * from "./firefox-previews";
8
- export * from "./manifest";
9
- export * from "./package";
10
- export * from "./safari";
11
- export * from "./scaffold";
12
- export * from "./sanitize";
13
- export * from "./types";
1
+ export*from"./build";export*from"./app-store-connect";export*from"./app-store-submission";export*from"./chrome-web-store";export*from"./config";export*from"./firefox-addons";export*from"./firefox-previews";export*from"./manifest";export*from"./package";export*from"./safari";export*from"./scaffold";export*from"./sanitize";export*from"./types";
package/dist/manifest.js CHANGED
@@ -1,70 +1 @@
1
- import { basename } from "node:path";
2
- export function contentScriptOut(entry, out) {
3
- return out ?? basename(entry).replace(/\.[cm]?tsx?$/, ".js");
4
- }
5
- export function generateManifest(config, opts) {
6
- const target = opts.target ?? "chrome", isFirefox = target === "firefox", isSafari = target === "safari", m = config.manifest ?? {}, manifest = {
7
- manifest_version: 3,
8
- name: config.name,
9
- description: config.description,
10
- version: opts.version
11
- };
12
- if (!isFirefox && !isSafari && m.minimumChromeVersion)
13
- manifest.minimum_chrome_version = m.minimumChromeVersion;
14
- if (config.pages?.popup || config.actionIcons)
15
- manifest.action = {
16
- default_title: config.name,
17
- ...config.pages?.popup ? { default_popup: "popup.html" } : {},
18
- ...config.actionIcons ? { default_icon: Object.fromEntries(Object.entries(config.actionIcons).map(([size, path]) => [String(size), path])) } : {}
19
- };
20
- if (config.pages?.options)
21
- manifest.options_page = "options.html";
22
- if (config.background)
23
- manifest.background = isFirefox ? { scripts: ["background.js"], type: "module" } : isSafari ? { service_worker: "background.js" } : { service_worker: "background.js", type: "module" };
24
- if (isFirefox && config.geckoId)
25
- manifest.browser_specific_settings = {
26
- gecko: {
27
- id: config.geckoId,
28
- strict_min_version: m.firefoxMinVersion ?? "142.0",
29
- data_collection_permissions: { required: ["none"] }
30
- }
31
- };
32
- if (isSafari)
33
- manifest.browser_specific_settings = {
34
- safari: {
35
- strict_min_version: m.safariMinVersion ?? "18.4"
36
- }
37
- };
38
- if (m.permissions?.length)
39
- manifest.permissions = m.permissions;
40
- if (m.optionalPermissions?.length)
41
- manifest.optional_permissions = m.optionalPermissions;
42
- if (m.hostPermissions?.length)
43
- manifest.host_permissions = m.hostPermissions;
44
- if (config.icons)
45
- manifest.icons = Object.fromEntries(Object.entries(config.icons).map(([size, path]) => [String(size), path]));
46
- if (config.content?.length)
47
- manifest.content_scripts = config.content.map((cs) => ({
48
- matches: cs.matches,
49
- js: [contentScriptOut(cs.entry, cs.out)],
50
- ...cs.runAt ? { run_at: cs.runAt } : {},
51
- ...cs.world ? { world: cs.world } : {},
52
- ...cs.allFrames ? { all_frames: !0 } : {},
53
- ...cs.matchAboutBlank ? { match_about_blank: !0 } : {},
54
- ...cs.excludeMatches ? { exclude_matches: cs.excludeMatches } : {}
55
- }));
56
- if (config.rules?.length)
57
- manifest.declarative_net_request = {
58
- rule_resources: config.rules.map((r) => ({
59
- id: r.id,
60
- enabled: r.enabled ?? !0,
61
- path: r.path ?? `rules/${r.id}.json`
62
- }))
63
- };
64
- manifest.content_security_policy = {
65
- extension_pages: m.contentSecurityPolicy ?? "script-src 'self'; object-src 'self'"
66
- };
67
- if (m.webAccessibleResources?.length)
68
- manifest.web_accessible_resources = m.webAccessibleResources;
69
- return { ...manifest, ...m.extra ?? {} };
70
- }
1
+ import{basename}from"node:path";export function contentScriptOut(entry,out){return out??basename(entry).replace(/\.[cm]?tsx?$/,".js")}export function generateManifest(config,opts){const target=opts.target??"chrome",isFirefox=target==="firefox",isSafari=target==="safari",m=config.manifest??{},manifest={manifest_version:3,name:config.name,description:config.description,version:opts.version};if(!isFirefox&&!isSafari&&m.minimumChromeVersion)manifest.minimum_chrome_version=m.minimumChromeVersion;if(config.pages?.popup||config.actionIcons)manifest.action={default_title:config.name,...config.pages?.popup?{default_popup:"popup.html"}:{},...config.actionIcons?{default_icon:Object.fromEntries(Object.entries(config.actionIcons).map(([size,path])=>[String(size),path]))}:{}};if(config.pages?.options)manifest.options_page="options.html";if(config.background)manifest.background=isFirefox?{scripts:["background.js"],type:"module"}:isSafari?{service_worker:"background.js"}:{service_worker:"background.js",type:"module"};if(isFirefox&&config.geckoId)manifest.browser_specific_settings={gecko:{id:config.geckoId,strict_min_version:m.firefoxMinVersion??"142.0",data_collection_permissions:{required:["none"]}}};if(isSafari)manifest.browser_specific_settings={safari:{strict_min_version:m.safariMinVersion??"18.4"}};if(m.permissions?.length)manifest.permissions=m.permissions;if(m.optionalPermissions?.length)manifest.optional_permissions=m.optionalPermissions;if(m.hostPermissions?.length)manifest.host_permissions=m.hostPermissions;if(config.icons)manifest.icons=Object.fromEntries(Object.entries(config.icons).map(([size,path])=>[String(size),path]));if(config.content?.length)manifest.content_scripts=config.content.map((cs)=>({matches:cs.matches,js:[contentScriptOut(cs.entry,cs.out)],...cs.runAt?{run_at:cs.runAt}:{},...cs.world?{world:cs.world}:{},...cs.allFrames?{all_frames:!0}:{},...cs.matchAboutBlank?{match_about_blank:!0}:{},...cs.excludeMatches?{exclude_matches:cs.excludeMatches}:{}}));if(config.rules?.length)manifest.declarative_net_request={rule_resources:config.rules.map((r)=>({id:r.id,enabled:r.enabled??!0,path:r.path??`rules/${r.id}.json`}))};manifest.content_security_policy={extension_pages:m.contentSecurityPolicy??"script-src 'self'; object-src 'self'"};if(m.webAccessibleResources?.length)manifest.web_accessible_resources=m.webAccessibleResources;return{...manifest,...m.extra??{}}}
package/dist/package.js CHANGED
@@ -1,21 +1 @@
1
- import { existsSync } from "node:fs";
2
- import { mkdir } from "node:fs/promises";
3
- import { dirname, resolve } from "node:path";
4
- import { buildExtension, resolveOutdir } from "./build";
5
- function slug(name) {
6
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
7
- }
8
- export async function packageExtension(config, options) {
9
- const target = options.target ?? "chrome", cwd = options.cwd ?? process.cwd();
10
- if (options.build !== !1)
11
- await buildExtension(config, { target, version: options.version, cwd });
12
- const outdir = resolve(cwd, resolveOutdir(config, target));
13
- if (!existsSync(outdir))
14
- throw Error(`[browser-extension] nothing to package: ${outdir} does not exist`);
15
- const suffix = target === "chrome" ? "" : `-${target}`, outfile = resolve(cwd, options.outfile ?? `${slug(config.name)}-${options.version}${suffix}.zip`);
16
- await mkdir(dirname(outfile), { recursive: !0 });
17
- const proc = Bun.spawn(["zip", "-r", "-q", outfile, "."], { cwd: outdir, stdout: "pipe", stderr: "pipe" }), code = await proc.exited;
18
- if (code !== 0)
19
- throw Error(`[browser-extension] zip failed (${code}): ${await new Response(proc.stderr).text()}`);
20
- return outfile;
21
- }
1
+ import{existsSync}from"node:fs";import{mkdir}from"node:fs/promises";import{dirname,resolve}from"node:path";import{buildExtension,resolveOutdir}from"./build";function slug(name){return name.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/(^-|-$)/g,"")}export async function packageExtension(config,options){const target=options.target??"chrome",cwd=options.cwd??process.cwd();if(options.build!==!1)await buildExtension(config,{target,version:options.version,cwd});const outdir=resolve(cwd,resolveOutdir(config,target));if(!existsSync(outdir))throw Error(`[browser-extension] nothing to package: ${outdir} does not exist`);const suffix=target==="chrome"?"":`-${target}`,outfile=resolve(cwd,options.outfile??`${slug(config.name)}-${options.version}${suffix}.zip`);await mkdir(dirname(outfile),{recursive:!0});const proc=Bun.spawn(["zip","-r","-q",outfile,"."],{cwd:outdir,stdout:"pipe",stderr:"pipe"}),code=await proc.exited;if(code!==0)throw Error(`[browser-extension] zip failed (${code}): ${await new Response(proc.stderr).text()}`);return outfile}