@stacksjs/browser-extension 0.70.257 → 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.
package/dist/safari.js CHANGED
@@ -1,52 +1,4 @@
1
- import { cpSync, existsSync, readdirSync, statSync } from "node:fs";
2
- import { mkdir, rm } from "node:fs/promises";
3
- import { dirname, join, resolve } from "node:path";
4
- import { buildExtension, resolveOutdir } from "./build";
5
- export function safariAppName(config) {
6
- const name = config.name.replace(/[^a-z0-9]+/gi, "");
7
- if (!name)
8
- throw Error("[browser-extension] cannot derive an app name from the extension name; set one without spaces");
9
- return name;
10
- }
11
- export function safariProjectDir(cwd = process.cwd(), dir = "safari") {
12
- return resolve(cwd, dir);
13
- }
14
- export function resolveSafariPlatforms(config, platforms) {
15
- const selected = platforms ?? config.safariPlatforms ?? ["macos"];
16
- return [...new Set(selected)];
17
- }
18
- function safariPlatformFlag(platforms) {
19
- if (platforms.length !== 1)
20
- return;
21
- return platforms[0] === "ios" ? "--ios-only" : "--macos-only";
22
- }
23
- export function safariPackagerArgs(input, projectLocation, appName, bundleId, platforms) {
24
- const platformFlag = safariPlatformFlag(platforms);
25
- return [
26
- "safari-web-extension-packager",
27
- "--project-location",
28
- projectLocation,
29
- "--app-name",
30
- appName,
31
- "--bundle-identifier",
32
- bundleId,
33
- "--swift",
34
- "--copy-resources",
35
- "--no-open",
36
- "--no-prompt",
37
- "--force",
38
- ...platformFlag ? [platformFlag] : [],
39
- input
40
- ];
41
- }
42
- function templateRoot() {
43
- return resolve(import.meta.dir, "..", "safari-template");
44
- }
45
- function fillTemplate(text, vars) {
46
- return text.replaceAll("__EXT_BUNDLE_ID__", vars.extBundleId).replaceAll("__BUNDLE_ID__", vars.bundleId).replaceAll("__APP_DISPLAY_NAME__", vars.displayName).replaceAll("__APP_NAME__", vars.appName).replaceAll("__MARKETING_VERSION__", vars.version).replaceAll("__DEVELOPMENT_TEAM__", vars.teamId).replaceAll("__YEAR__", String(new Date().getFullYear()));
47
- }
48
- function safariResourceCopyBuildPhase(appName) {
49
- return `/* Begin PBXShellScriptBuildPhase section */
1
+ import{cpSync,existsSync,readdirSync,statSync}from"node:fs";import{mkdir,rm}from"node:fs/promises";import{dirname,join,resolve}from"node:path";import{buildExtension,resolveOutdir}from"./build";export function safariAppName(config){const name=config.name.replace(/[^a-z0-9]+/gi,"");if(!name)throw Error("[browser-extension] cannot derive an app name from the extension name; set one without spaces");return name}export function safariProjectDir(cwd=process.cwd(),dir="safari"){return resolve(cwd,dir)}export function resolveSafariPlatforms(config,platforms){const selected=platforms??config.safariPlatforms??["macos"];return[...new Set(selected)]}function safariPlatformFlag(platforms){if(platforms.length!==1)return;return platforms[0]==="ios"?"--ios-only":"--macos-only"}export function safariPackagerArgs(input,projectLocation,appName,bundleId,platforms){const platformFlag=safariPlatformFlag(platforms);return["safari-web-extension-packager","--project-location",projectLocation,"--app-name",appName,"--bundle-identifier",bundleId,"--swift","--copy-resources","--no-open","--no-prompt","--force",...platformFlag?[platformFlag]:[],input]}function templateRoot(){return resolve(import.meta.dir,"..","safari-template")}function fillTemplate(text,vars){return text.replaceAll("__EXT_BUNDLE_ID__",vars.extBundleId).replaceAll("__BUNDLE_ID__",vars.bundleId).replaceAll("__APP_DISPLAY_NAME__",vars.displayName).replaceAll("__APP_NAME__",vars.appName).replaceAll("__MARKETING_VERSION__",vars.version).replaceAll("__DEVELOPMENT_TEAM__",vars.teamId).replaceAll("__YEAR__",String(new Date().getFullYear()))}function safariResourceCopyBuildPhase(appName){return`/* Begin PBXShellScriptBuildPhase section */
50
2
  0D0000000000000000000023 /* Copy Web Extension Resources */ = {
51
3
  isa = PBXShellScriptBuildPhase;
52
4
  alwaysOutOfDate = 1;
@@ -66,238 +18,14 @@ function safariResourceCopyBuildPhase(appName) {
66
18
  };
67
19
  /* End PBXShellScriptBuildPhase section */
68
20
 
69
- `;
70
- }
71
- export function migrateSafariResourceBuildPhase(project, appName) {
72
- if (project.includes("resources_prefix='$(SRCROOT)"))
73
- return project.replace(/\/\* Begin PBXShellScriptBuildPhase section \*\/[\s\S]*?\/\* End PBXShellScriptBuildPhase section \*\/\n/, safariResourceCopyBuildPhase(appName));
74
- if (project.includes("SCRIPT_INPUT_FILE_LIST_0"))
75
- return project;
76
- if (project.includes("0D0000000000000000000023 /* Copy Web Extension Resources */"))
77
- return project.replace(/\/\* Begin PBXShellScriptBuildPhase section \*\/[\s\S]*?\/\* End PBXShellScriptBuildPhase section \*\/\n/, safariResourceCopyBuildPhase(appName));
78
- if (!project.includes("0C0000000000000000000023 /* Resources in Resources */"))
79
- return project;
80
- return project.replace(/^\s*0C0000000000000000000023 \/\* Resources in Resources \*\/.*\n/m, "").replace(/^\s*0C0000000000000000000023 \/\* Resources in Resources \*\/,\n/mg, "").replace(` 0D0000000000000000000022 /* Resources */,
81
- `, ` 0D0000000000000000000022 /* Resources */,
21
+ `}export function migrateSafariResourceBuildPhase(project,appName){if(project.includes("resources_prefix='$(SRCROOT)"))return project.replace(/\/\* Begin PBXShellScriptBuildPhase section \*\/[\s\S]*?\/\* End PBXShellScriptBuildPhase section \*\/\n/,safariResourceCopyBuildPhase(appName));if(project.includes("SCRIPT_INPUT_FILE_LIST_0"))return project;if(project.includes("0D0000000000000000000023 /* Copy Web Extension Resources */"))return project.replace(/\/\* Begin PBXShellScriptBuildPhase section \*\/[\s\S]*?\/\* End PBXShellScriptBuildPhase section \*\/\n/,safariResourceCopyBuildPhase(appName));if(!project.includes("0C0000000000000000000023 /* Resources in Resources */"))return project;return project.replace(/^\s*0C0000000000000000000023 \/\* Resources in Resources \*\/.*\n/m,"").replace(/^\s*0C0000000000000000000023 \/\* Resources in Resources \*\/,\n/mg,"").replace(` 0D0000000000000000000022 /* Resources */,
22
+ `,` 0D0000000000000000000022 /* Resources */,
82
23
  0D0000000000000000000023 /* Copy Web Extension Resources */,
83
- `).replace("/* Begin PBXSourcesBuildPhase section */", `${safariResourceCopyBuildPhase(appName)}/* Begin PBXSourcesBuildPhase section */`);
84
- }
85
- function* walk(dir, prefix = "") {
86
- for (const entry of readdirSync(dir).sort()) {
87
- const rel = prefix ? `${prefix}/${entry}` : entry;
88
- if (statSync(join(dir, entry)).isDirectory())
89
- yield* walk(join(dir, entry), rel);
90
- else
91
- yield rel;
92
- }
93
- }
94
- export async function scaffoldSafariApp(config, options = {}) {
95
- const cwd = options.cwd ?? process.cwd(), appName = safariAppName(config), bundleId = options.bundleId ?? config.safariBundleId ?? `com.example.${appName.toLowerCase()}`, dir = safariProjectDir(cwd, options.dir), vars = {
96
- appName,
97
- displayName: config.name,
98
- bundleId,
99
- extBundleId: `${bundleId}.Extension`,
100
- version: options.version ?? "0.1.0",
101
- teamId: options.teamId ?? config.safariTeamId ?? ""
102
- };
103
- if (!options.bundleId && !config.safariBundleId)
104
- console.warn(`[browser-extension] no safari bundle id configured; using placeholder ${bundleId}. Set safariBundleId in config/extension.ts.`);
105
- const written = [], skipped = [];
106
- for (const rel of walk(templateRoot())) {
107
- const outRel = fillTemplate(rel, vars), out = join(dir, outRel);
108
- if (existsSync(out) && !options.force) {
109
- skipped.push(outRel);
110
- continue;
111
- }
112
- await mkdir(dirname(out), { recursive: !0 });
113
- await Bun.write(out, fillTemplate(await Bun.file(join(templateRoot(), rel)).text(), vars));
114
- written.push(outRel);
115
- }
116
- await generateAppIcons(config, dir, vars, options.iconsDir, cwd);
117
- return { dir, written, skipped };
118
- }
119
- const appIconSizes = [16, 32, 64, 128, 256, 512, 1024];
120
- async function generateAppIcons(config, dir, vars, iconsDir, cwd) {
121
- const source = [
122
- iconsDir,
123
- join(resolve(cwd, resolveOutdir(config, "chrome")), "icons"),
124
- config.public ? join(resolve(cwd, config.public), "icons") : void 0
125
- ].filter(Boolean).find((d) => existsSync(d));
126
- if (!source) {
127
- console.warn("[browser-extension] no icons directory found; add PNGs to the asset catalog manually.");
128
- return;
129
- }
130
- let largest, largestSize = 0;
131
- for (const file of readdirSync(source)) {
132
- const match = /(\d+)\.png$/.exec(file);
133
- if (match && Number(match[1]) > largestSize) {
134
- largestSize = Number(match[1]);
135
- largest = join(source, file);
136
- }
137
- }
138
- if (!largest) {
139
- console.warn(`[browser-extension] no icon PNGs in ${source}; add them to the asset catalog manually.`);
140
- return;
141
- }
142
- const appIconDir = join(dir, vars.appName, "Assets.xcassets", "AppIcon.appiconset"), imageSetDir = join(dir, vars.appName, "Assets.xcassets", "ExtensionIcon.imageset");
143
- await mkdir(appIconDir, { recursive: !0 });
144
- await mkdir(imageSetDir, { recursive: !0 });
145
- const targets = [
146
- ...appIconSizes.map((size) => ({ dir: appIconDir, name: `icon-${size}.png`, size })),
147
- { dir: imageSetDir, name: "extension-icon-128.png", size: 128 },
148
- { dir: imageSetDir, name: "extension-icon-256.png", size: 256 }
149
- ];
150
- for (const t of targets)
151
- if (await Bun.spawn(["sips", "-z", String(t.size), String(t.size), largest, "--out", join(t.dir, t.name)], { stdout: "ignore", stderr: "pipe" }).exited !== 0) {
152
- console.warn(`[browser-extension] sips failed for ${t.name} (macOS only). Generate the icon PNGs on a Mac.`);
153
- return;
154
- }
155
- }
156
- async function mirrorSafariResources(config, outdir, resources, keepPlaceholder = !1) {
157
- const exclude = new Set(config.safariExclude ?? []);
158
- await rm(resources, { recursive: !0, force: !0 });
159
- await mkdir(resources, { recursive: !0 });
160
- if (keepPlaceholder)
161
- await Bun.write(join(resources, ".gitkeep"), "");
162
- const synced = [];
163
- for (const rel of walk(outdir)) {
164
- if (exclude.has(rel) || rel.split("/").includes(".DS_Store"))
165
- continue;
166
- const dest = join(resources, rel);
167
- await mkdir(dirname(dest), { recursive: !0 });
168
- cpSync(join(outdir, rel), dest);
169
- synced.push(rel);
170
- }
171
- return synced;
172
- }
173
- export async function syncSafariResources(config, options = {}) {
174
- const cwd = options.cwd ?? process.cwd(), outdir = resolve(cwd, options.outdir ?? resolveOutdir(config, "safari"));
175
- if (!existsSync(join(outdir, "manifest.json")))
176
- throw Error(`[browser-extension] ${outdir}/manifest.json is missing. Run extension:build --target safari first.`);
177
- const appName = safariAppName(config), projectDir = safariProjectDir(cwd, options.dir), projectPath = join(projectDir, `${appName}.xcodeproj`, "project.pbxproj");
178
- if (existsSync(projectPath)) {
179
- const project = await Bun.file(projectPath).text(), migrated = migrateSafariResourceBuildPhase(project, appName);
180
- if (migrated !== project)
181
- await Bun.write(projectPath, migrated);
182
- }
183
- const resources = join(projectDir, `${appName} Extension`, "Resources"), synced = await mirrorSafariResources(config, outdir, resources, !0), inputs = synced.map((rel) => `$(SRCROOT)/${appName} Extension/Resources/${rel}`).join(`
184
- `), outputs = synced.map((rel) => `$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/${rel}`).join(`
185
- `);
186
- await Bun.write(join(projectDir, `${appName} Extension`, "Resources.inputs.xcfilelist"), `${inputs}
187
- `);
188
- await Bun.write(join(projectDir, `${appName} Extension`, "Resources.outputs.xcfilelist"), `${outputs}
189
- `);
190
- return { resources, files: synced.length };
191
- }
192
- export async function createSafariUniversalProject(config, options = {}) {
193
- const cwd = options.cwd ?? process.cwd();
194
- if (options.build !== !1) {
195
- if (!options.version)
196
- throw Error("[browser-extension] createSafariUniversalProject needs a version to build the extension");
197
- await buildExtension(config, { target: "safari", version: options.version, cwd });
198
- }
199
- const outdir = resolve(cwd, options.outdir ?? resolveOutdir(config, "safari"));
200
- if (!existsSync(join(outdir, "manifest.json")))
201
- throw Error(`[browser-extension] ${outdir}/manifest.json is missing. Run extension:build --target safari first.`);
202
- const appName = safariAppName(config), bundleId = config.safariBundleId;
203
- if (!bundleId)
204
- throw Error("[browser-extension] universal Safari packaging needs safariBundleId in config/extension.ts");
205
- const platforms = resolveSafariPlatforms(config, options.platforms);
206
- if (!platforms.length)
207
- throw Error("[browser-extension] universal Safari packaging needs at least one platform");
208
- const buildDir = join(safariProjectDir(cwd, options.dir), "build"), resources = join(buildDir, "universal-resources"), projectLocation = join(buildDir, "universal-project");
209
- await mirrorSafariResources(config, outdir, resources);
210
- await rm(projectLocation, { recursive: !0, force: !0 });
211
- await mkdir(projectLocation, { recursive: !0 });
212
- const args = safariPackagerArgs(resources, projectLocation, appName, bundleId, platforms), exitCode = await Bun.spawn(["xcrun", ...args], { cwd, stdout: "inherit", stderr: "inherit" }).exited;
213
- if (exitCode !== 0)
214
- throw Error(`[browser-extension] Safari web extension packager failed (${exitCode})`);
215
- const dir = join(projectLocation, appName), project = join(dir, `${appName}.xcodeproj`);
216
- if (!existsSync(project))
217
- throw Error(`[browser-extension] Safari packager did not create ${project}`);
218
- return { dir, project, resources, platforms };
219
- }
220
- export async function buildSafariUniversalApp(config, options = {}) {
221
- const generated = await createSafariUniversalProject(config, options), appPaths = {};
222
- if (options.skipXcodebuild)
223
- return { ...generated, appPaths };
224
- const appName = safariAppName(config), configuration = options.release ? "Release" : "Debug", derivedData = join(safariProjectDir(options.cwd ?? process.cwd(), options.dir), "build", "universal-derived");
225
- await rm(derivedData, { recursive: !0, force: !0 });
226
- for (const platform of generated.platforms) {
227
- const ios = platform === "ios", args = [
228
- "xcodebuild",
229
- "-project",
230
- generated.project,
231
- "-scheme",
232
- `${appName} (${ios ? "iOS" : "macOS"})`,
233
- "-configuration",
234
- configuration,
235
- "-destination",
236
- ios ? "generic/platform=iOS Simulator" : "generic/platform=macOS",
237
- "-derivedDataPath",
238
- derivedData,
239
- ...ios ? ["-sdk", "iphonesimulator", "CODE_SIGNING_ALLOWED=NO"] : options.signed ? ["-allowProvisioningUpdates"] : ["CODE_SIGNING_ALLOWED=NO"],
240
- "build"
241
- ], exitCode = await Bun.spawn(args, { cwd: generated.dir, stdout: "inherit", stderr: "inherit" }).exited;
242
- if (exitCode !== 0)
243
- throw Error(`[browser-extension] Safari ${platform} app build failed (${exitCode})`);
244
- const products = ios ? `${configuration}-iphonesimulator` : configuration;
245
- appPaths[platform] = join(derivedData, "Build", "Products", products, `${appName}.app`);
246
- }
247
- return { ...generated, appPaths };
248
- }
249
- export async function buildSafariApp(config, options = {}) {
250
- const cwd = options.cwd ?? process.cwd();
251
- if (options.build !== !1) {
252
- if (!options.version)
253
- throw Error("[browser-extension] buildSafariApp needs a version to build the extension");
254
- await buildExtension(config, { target: "safari", version: options.version, cwd });
255
- }
256
- const { resources } = await syncSafariResources(config, options);
257
- if (options.skipXcodebuild)
258
- return { resources };
259
- const appName = safariAppName(config), dir = safariProjectDir(cwd, options.dir), configuration = options.release ? "Release" : "Debug", derivedData = join(dir, "build"), developerDir = (process.env.DEVELOPER_DIR ?? (await Bun.$`xcode-select -p`.text()).trim()).replace(/\/$/, "");
260
- if (!developerDir.includes(".app/Contents/Developer")) {
261
- console.error(`[browser-extension] full Xcode is required to build the app (active developer directory: ${developerDir}).`);
262
- console.error("Install Xcode, then: sudo xcode-select -s /Applications/Xcode.app/Contents/Developer");
263
- console.error("or point DEVELOPER_DIR at any Xcode toolchain (betas included) for this shell.");
264
- console.error(`The extension payload is built and synced at ${resources}`);
265
- return { resources };
266
- }
267
- const signing = options.signed ? ["-allowProvisioningUpdates"] : ["CODE_SIGNING_ALLOWED=NO"];
268
- await Bun.$`xcodebuild -project ${join(dir, `${appName}.xcodeproj`)} -scheme ${appName} -configuration ${configuration} -derivedDataPath ${derivedData} ${signing} build`;
269
- return { appPath: join(derivedData, "Build", "Products", configuration, `${appName}.app`), resources };
270
- }
271
- export function resolveAppStoreConnectAuth(options) {
272
- const keyId = options.keyId ?? process.env.APP_STORE_CONNECT_API_KEY_ID, issuerId = options.issuerId ?? process.env.APP_STORE_CONNECT_API_ISSUER_ID, keyPath = options.keyPath ?? process.env.APP_STORE_CONNECT_API_KEY_PATH, missing = [
273
- !keyId && "APP_STORE_CONNECT_API_KEY_ID",
274
- !issuerId && "APP_STORE_CONNECT_API_ISSUER_ID",
275
- !keyPath && "APP_STORE_CONNECT_API_KEY_PATH"
276
- ].filter(Boolean);
277
- if (missing.length)
278
- throw Error(`[browser-extension] missing App Store Connect credentials: ${missing.join(", ")}`);
279
- if (!existsSync(resolve(keyPath)))
280
- throw Error(`[browser-extension] App Store Connect API key not found: ${resolve(keyPath)}`);
281
- return { keyId, issuerId, keyPath: resolve(keyPath) };
282
- }
283
- function xcodeAuthArgs(auth) {
284
- return [
285
- "-allowProvisioningUpdates",
286
- "-authenticationKeyPath",
287
- auth.keyPath,
288
- "-authenticationKeyID",
289
- auth.keyId,
290
- "-authenticationKeyIssuerID",
291
- auth.issuerId
292
- ];
293
- }
294
- async function runXcodebuild(args, cwd, operation) {
295
- const exitCode = await Bun.spawn(["xcodebuild", ...args], { cwd, stdout: "inherit", stderr: "inherit" }).exited;
296
- if (exitCode !== 0)
297
- throw Error(`[browser-extension] ${operation} failed (${exitCode})`);
298
- }
299
- function exportOptionsPlist(method, teamId) {
300
- return `<?xml version="1.0" encoding="UTF-8"?>
24
+ `).replace("/* Begin PBXSourcesBuildPhase section */",`${safariResourceCopyBuildPhase(appName)}/* Begin PBXSourcesBuildPhase section */`)}function*walk(dir,prefix=""){for(const entry of readdirSync(dir).sort()){const rel=prefix?`${prefix}/${entry}`:entry;if(statSync(join(dir,entry)).isDirectory())yield*walk(join(dir,entry),rel);else yield rel}}export async function scaffoldSafariApp(config,options={}){const cwd=options.cwd??process.cwd(),appName=safariAppName(config),bundleId=options.bundleId??config.safariBundleId??`com.example.${appName.toLowerCase()}`,dir=safariProjectDir(cwd,options.dir),vars={appName,displayName:config.name,bundleId,extBundleId:`${bundleId}.Extension`,version:options.version??"0.1.0",teamId:options.teamId??config.safariTeamId??""};if(!options.bundleId&&!config.safariBundleId)console.warn(`[browser-extension] no safari bundle id configured; using placeholder ${bundleId}. Set safariBundleId in config/extension.ts.`);const written=[],skipped=[];for(const rel of walk(templateRoot())){const outRel=fillTemplate(rel,vars),out=join(dir,outRel);if(existsSync(out)&&!options.force){skipped.push(outRel);continue}await mkdir(dirname(out),{recursive:!0});await Bun.write(out,fillTemplate(await Bun.file(join(templateRoot(),rel)).text(),vars));written.push(outRel)}await generateAppIcons(config,dir,vars,options.iconsDir,cwd);return{dir,written,skipped}}const appIconSizes=[16,32,64,128,256,512,1024];async function generateAppIcons(config,dir,vars,iconsDir,cwd){const source=[iconsDir,join(resolve(cwd,resolveOutdir(config,"chrome")),"icons"),config.public?join(resolve(cwd,config.public),"icons"):void 0].filter(Boolean).find((d)=>existsSync(d));if(!source){console.warn("[browser-extension] no icons directory found; add PNGs to the asset catalog manually.");return}let largest,largestSize=0;for(const file of readdirSync(source)){const match=/(\d+)\.png$/.exec(file);if(match&&Number(match[1])>largestSize){largestSize=Number(match[1]);largest=join(source,file)}}if(!largest){console.warn(`[browser-extension] no icon PNGs in ${source}; add them to the asset catalog manually.`);return}const appIconDir=join(dir,vars.appName,"Assets.xcassets","AppIcon.appiconset"),imageSetDir=join(dir,vars.appName,"Assets.xcassets","ExtensionIcon.imageset");await mkdir(appIconDir,{recursive:!0});await mkdir(imageSetDir,{recursive:!0});const targets=[...appIconSizes.map((size)=>({dir:appIconDir,name:`icon-${size}.png`,size})),{dir:imageSetDir,name:"extension-icon-128.png",size:128},{dir:imageSetDir,name:"extension-icon-256.png",size:256}];for(const t of targets)if(await Bun.spawn(["sips","-z",String(t.size),String(t.size),largest,"--out",join(t.dir,t.name)],{stdout:"ignore",stderr:"pipe"}).exited!==0){console.warn(`[browser-extension] sips failed for ${t.name} (macOS only). Generate the icon PNGs on a Mac.`);return}}async function mirrorSafariResources(config,outdir,resources,keepPlaceholder=!1){const exclude=new Set(config.safariExclude??[]);await rm(resources,{recursive:!0,force:!0});await mkdir(resources,{recursive:!0});if(keepPlaceholder)await Bun.write(join(resources,".gitkeep"),"");const synced=[];for(const rel of walk(outdir)){if(exclude.has(rel)||rel.split("/").includes(".DS_Store"))continue;const dest=join(resources,rel);await mkdir(dirname(dest),{recursive:!0});cpSync(join(outdir,rel),dest);synced.push(rel)}return synced}export async function syncSafariResources(config,options={}){const cwd=options.cwd??process.cwd(),outdir=resolve(cwd,options.outdir??resolveOutdir(config,"safari"));if(!existsSync(join(outdir,"manifest.json")))throw Error(`[browser-extension] ${outdir}/manifest.json is missing. Run extension:build --target safari first.`);const appName=safariAppName(config),projectDir=safariProjectDir(cwd,options.dir),projectPath=join(projectDir,`${appName}.xcodeproj`,"project.pbxproj");if(existsSync(projectPath)){const project=await Bun.file(projectPath).text(),migrated=migrateSafariResourceBuildPhase(project,appName);if(migrated!==project)await Bun.write(projectPath,migrated)}const resources=join(projectDir,`${appName} Extension`,"Resources"),synced=await mirrorSafariResources(config,outdir,resources,!0),inputs=synced.map((rel)=>`$(SRCROOT)/${appName} Extension/Resources/${rel}`).join(`
25
+ `),outputs=synced.map((rel)=>`$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/${rel}`).join(`
26
+ `);await Bun.write(join(projectDir,`${appName} Extension`,"Resources.inputs.xcfilelist"),`${inputs}
27
+ `);await Bun.write(join(projectDir,`${appName} Extension`,"Resources.outputs.xcfilelist"),`${outputs}
28
+ `);return{resources,files:synced.length}}export async function createSafariUniversalProject(config,options={}){const cwd=options.cwd??process.cwd();if(options.build!==!1){if(!options.version)throw Error("[browser-extension] createSafariUniversalProject needs a version to build the extension");await buildExtension(config,{target:"safari",version:options.version,cwd})}const outdir=resolve(cwd,options.outdir??resolveOutdir(config,"safari"));if(!existsSync(join(outdir,"manifest.json")))throw Error(`[browser-extension] ${outdir}/manifest.json is missing. Run extension:build --target safari first.`);const appName=safariAppName(config),bundleId=config.safariBundleId;if(!bundleId)throw Error("[browser-extension] universal Safari packaging needs safariBundleId in config/extension.ts");const platforms=resolveSafariPlatforms(config,options.platforms);if(!platforms.length)throw Error("[browser-extension] universal Safari packaging needs at least one platform");const buildDir=join(safariProjectDir(cwd,options.dir),"build"),resources=join(buildDir,"universal-resources"),projectLocation=join(buildDir,"universal-project");await mirrorSafariResources(config,outdir,resources);await rm(projectLocation,{recursive:!0,force:!0});await mkdir(projectLocation,{recursive:!0});const args=safariPackagerArgs(resources,projectLocation,appName,bundleId,platforms),exitCode=await Bun.spawn(["xcrun",...args],{cwd,stdout:"inherit",stderr:"inherit"}).exited;if(exitCode!==0)throw Error(`[browser-extension] Safari web extension packager failed (${exitCode})`);const dir=join(projectLocation,appName),project=join(dir,`${appName}.xcodeproj`);if(!existsSync(project))throw Error(`[browser-extension] Safari packager did not create ${project}`);return{dir,project,resources,platforms}}export async function buildSafariUniversalApp(config,options={}){const generated=await createSafariUniversalProject(config,options),appPaths={};if(options.skipXcodebuild)return{...generated,appPaths};const appName=safariAppName(config),configuration=options.release?"Release":"Debug",derivedData=join(safariProjectDir(options.cwd??process.cwd(),options.dir),"build","universal-derived");await rm(derivedData,{recursive:!0,force:!0});for(const platform of generated.platforms){const ios=platform==="ios",args=["xcodebuild","-project",generated.project,"-scheme",`${appName} (${ios?"iOS":"macOS"})`,"-configuration",configuration,"-destination",ios?"generic/platform=iOS Simulator":"generic/platform=macOS","-derivedDataPath",derivedData,...ios?["-sdk","iphonesimulator","CODE_SIGNING_ALLOWED=NO"]:options.signed?["-allowProvisioningUpdates"]:["CODE_SIGNING_ALLOWED=NO"],"build"],exitCode=await Bun.spawn(args,{cwd:generated.dir,stdout:"inherit",stderr:"inherit"}).exited;if(exitCode!==0)throw Error(`[browser-extension] Safari ${platform} app build failed (${exitCode})`);const products=ios?`${configuration}-iphonesimulator`:configuration;appPaths[platform]=join(derivedData,"Build","Products",products,`${appName}.app`)}return{...generated,appPaths}}export async function buildSafariApp(config,options={}){const cwd=options.cwd??process.cwd();if(options.build!==!1){if(!options.version)throw Error("[browser-extension] buildSafariApp needs a version to build the extension");await buildExtension(config,{target:"safari",version:options.version,cwd})}const{resources}=await syncSafariResources(config,options);if(options.skipXcodebuild)return{resources};const appName=safariAppName(config),dir=safariProjectDir(cwd,options.dir),configuration=options.release?"Release":"Debug",derivedData=join(dir,"build"),developerDir=(process.env.DEVELOPER_DIR??(await Bun.$`xcode-select -p`.text()).trim()).replace(/\/$/,"");if(!developerDir.includes(".app/Contents/Developer")){console.error(`[browser-extension] full Xcode is required to build the app (active developer directory: ${developerDir}).`);console.error("Install Xcode, then: sudo xcode-select -s /Applications/Xcode.app/Contents/Developer");console.error("or point DEVELOPER_DIR at any Xcode toolchain (betas included) for this shell.");console.error(`The extension payload is built and synced at ${resources}`);return{resources}}const signing=options.signed?["-allowProvisioningUpdates"]:["CODE_SIGNING_ALLOWED=NO"];await Bun.$`xcodebuild -project ${join(dir,`${appName}.xcodeproj`)} -scheme ${appName} -configuration ${configuration} -derivedDataPath ${derivedData} ${signing} build`;return{appPath:join(derivedData,"Build","Products",configuration,`${appName}.app`),resources}}export function resolveAppStoreConnectAuth(options){const keyId=options.keyId??process.env.APP_STORE_CONNECT_API_KEY_ID,issuerId=options.issuerId??process.env.APP_STORE_CONNECT_API_ISSUER_ID,keyPath=options.keyPath??process.env.APP_STORE_CONNECT_API_KEY_PATH,missing=[!keyId&&"APP_STORE_CONNECT_API_KEY_ID",!issuerId&&"APP_STORE_CONNECT_API_ISSUER_ID",!keyPath&&"APP_STORE_CONNECT_API_KEY_PATH"].filter(Boolean);if(missing.length)throw Error(`[browser-extension] missing App Store Connect credentials: ${missing.join(", ")}`);if(!existsSync(resolve(keyPath)))throw Error(`[browser-extension] App Store Connect API key not found: ${resolve(keyPath)}`);return{keyId,issuerId,keyPath:resolve(keyPath)}}function xcodeAuthArgs(auth){return["-allowProvisioningUpdates","-authenticationKeyPath",auth.keyPath,"-authenticationKeyID",auth.keyId,"-authenticationKeyIssuerID",auth.issuerId]}async function runXcodebuild(args,cwd,operation){const exitCode=await Bun.spawn(["xcodebuild",...args],{cwd,stdout:"inherit",stderr:"inherit"}).exited;if(exitCode!==0)throw Error(`[browser-extension] ${operation} failed (${exitCode})`)}function exportOptionsPlist(method,teamId){return`<?xml version="1.0" encoding="UTF-8"?>
301
29
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
302
30
  <plist version="1.0">
303
31
  <dict>
@@ -315,114 +43,4 @@ function exportOptionsPlist(method, teamId) {
315
43
  <true/>
316
44
  </dict>
317
45
  </plist>
318
- `;
319
- }
320
- export async function publishSafariApp(config, options) {
321
- const cwd = options.cwd ?? process.cwd(), teamId = options.teamId ?? config.safariTeamId;
322
- if (!teamId)
323
- throw Error("[browser-extension] Safari publishing needs safariTeamId in config/extension.ts or --team-id");
324
- const auth = resolveAppStoreConnectAuth(options), buildNumber = options.buildNumber ?? process.env.GITHUB_RUN_NUMBER ?? String(Math.floor(Date.now() / 1000));
325
- if (!/^\d+(?:\.\d+){0,2}$/.test(buildNumber))
326
- throw Error(`[browser-extension] invalid Safari build number ${buildNumber}; use one to three dot-separated integers`);
327
- const platforms = resolveSafariPlatforms(config, options.platforms);
328
- if (!platforms.length)
329
- throw Error("[browser-extension] Safari publishing needs at least one platform");
330
- const { AppStoreConnectClient, attachSafariBuilds, provisionSafariApp } = await import("./app-store-connect"), provisioned = await provisionSafariApp(config, {
331
- ...auth,
332
- version: options.validateOnly ? void 0 : options.version,
333
- platforms
334
- });
335
- if (!provisioned.appRecord.exists)
336
- throw Error("[browser-extension] the App Store Connect app record is missing; create it once in App Store Connect before publishing");
337
- const publishablePlatforms = options.validateOnly ? platforms : platforms.filter((platform) => provisioned.appStoreVersions.some((version) => version.platform === platform && version.status !== "deferred" && version.status !== "published")), deferred = provisioned.appStoreVersions.filter((version) => version.status === "deferred").map((version) => ({
338
- platform: version.platform,
339
- version: version.version,
340
- state: version.appStoreState,
341
- reason: version.reason ?? `Safari ${version.platform} publication is deferred`
342
- })), alreadyPublished = provisioned.appStoreVersions.filter((version) => version.status === "published").map((version) => ({ platform: version.platform, version: version.version, state: version.appStoreState }));
343
- if (!publishablePlatforms.length)
344
- return {
345
- archivePath: "",
346
- exportPath: "",
347
- buildNumber,
348
- artifacts: [],
349
- attachments: [],
350
- deferred,
351
- alreadyPublished
352
- };
353
- if (options.build !== !1)
354
- await buildExtension(config, { target: "safari", version: options.version, cwd });
355
- const appName = safariAppName(config), dir = safariProjectDir(cwd, options.dir), buildDir = join(dir, "build"), universal = publishablePlatforms.includes("ios");
356
- let project, projectCwd;
357
- if (universal) {
358
- const generated = await createSafariUniversalProject(config, {
359
- ...options,
360
- build: !1,
361
- platforms: publishablePlatforms
362
- });
363
- project = generated.project;
364
- projectCwd = generated.dir;
365
- } else {
366
- await syncSafariResources(config, options);
367
- project = join(dir, `${appName}.xcodeproj`);
368
- projectCwd = dir;
369
- }
370
- const authArgs = xcodeAuthArgs(auth);
371
- await mkdir(buildDir, { recursive: !0 });
372
- const method = options.validateOnly ? "validation" : "app-store-connect", artifacts = [];
373
- for (const platform of publishablePlatforms) {
374
- const suffix = universal ? `-${platform}` : "", archivePath = join(buildDir, `${appName}${suffix}.xcarchive`), exportPath = join(buildDir, `${options.validateOnly ? "validation" : "upload"}${suffix}`), plistPath = join(buildDir, `ExportOptions.${method}${suffix}.plist`), scheme = universal ? `${appName} (${platform === "ios" ? "iOS" : "macOS"})` : appName, destination = platform === "ios" ? "generic/platform=iOS" : "generic/platform=macOS";
375
- await rm(archivePath, { recursive: !0, force: !0 });
376
- await rm(exportPath, { recursive: !0, force: !0 });
377
- await runXcodebuild([
378
- "-project",
379
- project,
380
- "-scheme",
381
- scheme,
382
- "-configuration",
383
- "Release",
384
- "-destination",
385
- destination,
386
- "-archivePath",
387
- archivePath,
388
- `MARKETING_VERSION=${options.version}`,
389
- `CURRENT_PROJECT_VERSION=${buildNumber}`,
390
- `DEVELOPMENT_TEAM=${teamId}`,
391
- "INFOPLIST_KEY_ITSAppUsesNonExemptEncryption=NO",
392
- "CODE_SIGNING_ALLOWED=NO",
393
- ...platform === "macos" && config.safariAppCategory ? [`INFOPLIST_KEY_LSApplicationCategoryType=${config.safariAppCategory}`] : [],
394
- ...authArgs,
395
- "archive"
396
- ], projectCwd, `Safari ${platform} archive`);
397
- await Bun.write(plistPath, exportOptionsPlist(method, teamId));
398
- await runXcodebuild([
399
- "-exportArchive",
400
- "-archivePath",
401
- archivePath,
402
- "-exportPath",
403
- exportPath,
404
- "-exportOptionsPlist",
405
- plistPath,
406
- ...authArgs
407
- ], projectCwd, `Safari ${platform} export`);
408
- artifacts.push({ platform, archivePath, exportPath });
409
- }
410
- const attachments = options.validateOnly ? [] : await attachSafariBuilds(new AppStoreConnectClient(auth), provisioned.appRecord.id, provisioned.appStoreVersions.filter((version) => version.status !== "deferred" && version.status !== "published"), buildNumber, {
411
- timeoutMs: options.processingTimeoutMs,
412
- pollIntervalMs: options.processingPollIntervalMs
413
- }), appStoreSubmission = !options.validateOnly && config.safariAppStore ? await (await import("./app-store-submission")).prepareSafariAppStoreSubmission(new AppStoreConnectClient(auth), config, provisioned.appRecord.id, attachments.map((attachment) => ({
414
- platform: attachment.platform,
415
- id: attachment.versionId,
416
- buildId: attachment.buildId
417
- })), { cwd }) : void 0, firstArtifact = artifacts[0];
418
- return {
419
- archivePath: firstArtifact.archivePath,
420
- exportPath: firstArtifact.exportPath,
421
- buildNumber,
422
- artifacts,
423
- deferred,
424
- alreadyPublished,
425
- attachments,
426
- appStoreSubmission: appStoreSubmission ? { versions: appStoreSubmission.versions, reviewSubmissionIds: appStoreSubmission.reviewSubmissionIds } : void 0
427
- };
428
- }
46
+ `}export async function publishSafariApp(config,options){const cwd=options.cwd??process.cwd(),teamId=options.teamId??config.safariTeamId;if(!teamId)throw Error("[browser-extension] Safari publishing needs safariTeamId in config/extension.ts or --team-id");const auth=resolveAppStoreConnectAuth(options),buildNumber=options.buildNumber??process.env.GITHUB_RUN_NUMBER??String(Math.floor(Date.now()/1000));if(!/^\d+(?:\.\d+){0,2}$/.test(buildNumber))throw Error(`[browser-extension] invalid Safari build number ${buildNumber}; use one to three dot-separated integers`);const platforms=resolveSafariPlatforms(config,options.platforms);if(!platforms.length)throw Error("[browser-extension] Safari publishing needs at least one platform");const{AppStoreConnectClient,attachSafariBuilds,provisionSafariApp}=await import("./app-store-connect"),provisioned=await provisionSafariApp(config,{...auth,version:options.validateOnly?void 0:options.version,platforms});if(!provisioned.appRecord.exists)throw Error("[browser-extension] the App Store Connect app record is missing; create it once in App Store Connect before publishing");const publishablePlatforms=options.validateOnly?platforms:platforms.filter((platform)=>provisioned.appStoreVersions.some((version)=>version.platform===platform&&version.status!=="deferred"&&version.status!=="published")),deferred=provisioned.appStoreVersions.filter((version)=>version.status==="deferred").map((version)=>({platform:version.platform,version:version.version,state:version.appStoreState,reason:version.reason??`Safari ${version.platform} publication is deferred`})),alreadyPublished=provisioned.appStoreVersions.filter((version)=>version.status==="published").map((version)=>({platform:version.platform,version:version.version,state:version.appStoreState}));if(!publishablePlatforms.length)return{archivePath:"",exportPath:"",buildNumber,artifacts:[],attachments:[],deferred,alreadyPublished};if(options.build!==!1)await buildExtension(config,{target:"safari",version:options.version,cwd});const appName=safariAppName(config),dir=safariProjectDir(cwd,options.dir),buildDir=join(dir,"build"),universal=publishablePlatforms.includes("ios");let project,projectCwd;if(universal){const generated=await createSafariUniversalProject(config,{...options,build:!1,platforms:publishablePlatforms});project=generated.project;projectCwd=generated.dir}else{await syncSafariResources(config,options);project=join(dir,`${appName}.xcodeproj`);projectCwd=dir}const authArgs=xcodeAuthArgs(auth);await mkdir(buildDir,{recursive:!0});const method=options.validateOnly?"validation":"app-store-connect",artifacts=[];for(const platform of publishablePlatforms){const suffix=universal?`-${platform}`:"",archivePath=join(buildDir,`${appName}${suffix}.xcarchive`),exportPath=join(buildDir,`${options.validateOnly?"validation":"upload"}${suffix}`),plistPath=join(buildDir,`ExportOptions.${method}${suffix}.plist`),scheme=universal?`${appName} (${platform==="ios"?"iOS":"macOS"})`:appName,destination=platform==="ios"?"generic/platform=iOS":"generic/platform=macOS";await rm(archivePath,{recursive:!0,force:!0});await rm(exportPath,{recursive:!0,force:!0});await runXcodebuild(["-project",project,"-scheme",scheme,"-configuration","Release","-destination",destination,"-archivePath",archivePath,`MARKETING_VERSION=${options.version}`,`CURRENT_PROJECT_VERSION=${buildNumber}`,`DEVELOPMENT_TEAM=${teamId}`,"INFOPLIST_KEY_ITSAppUsesNonExemptEncryption=NO","CODE_SIGNING_ALLOWED=NO",...platform==="macos"&&config.safariAppCategory?[`INFOPLIST_KEY_LSApplicationCategoryType=${config.safariAppCategory}`]:[],...authArgs,"archive"],projectCwd,`Safari ${platform} archive`);await Bun.write(plistPath,exportOptionsPlist(method,teamId));await runXcodebuild(["-exportArchive","-archivePath",archivePath,"-exportPath",exportPath,"-exportOptionsPlist",plistPath,...authArgs],projectCwd,`Safari ${platform} export`);artifacts.push({platform,archivePath,exportPath})}const attachments=options.validateOnly?[]:await attachSafariBuilds(new AppStoreConnectClient(auth),provisioned.appRecord.id,provisioned.appStoreVersions.filter((version)=>version.status!=="deferred"&&version.status!=="published"),buildNumber,{timeoutMs:options.processingTimeoutMs,pollIntervalMs:options.processingPollIntervalMs}),appStoreSubmission=!options.validateOnly&&config.safariAppStore?await(await import("./app-store-submission")).prepareSafariAppStoreSubmission(new AppStoreConnectClient(auth),config,provisioned.appRecord.id,attachments.map((attachment)=>({platform:attachment.platform,id:attachment.versionId,buildId:attachment.buildId})),{cwd}):void 0,firstArtifact=artifacts[0];return{archivePath:firstArtifact.archivePath,exportPath:firstArtifact.exportPath,buildNumber,artifacts,deferred,alreadyPublished,attachments,appStoreSubmission:appStoreSubmission?{versions:appStoreSubmission.versions,reviewSubmissionIds:appStoreSubmission.reviewSubmissionIds}:void 0}}
package/dist/sanitize.js CHANGED
@@ -1,7 +1 @@
1
- export function sanitizeExtensionHtml(html, ownScripts) {
2
- const keep = ownScripts.map((s) => `(?![^>]*src="/?${s.replace(".", "\\.")}")`).join("");
3
- let out = html.replace(/\n?<!-- stx SEO Tags -->[\s\S]*?(?=\n\s*<meta charset=)/, "").replace(/\n?\s*<style\b[\s\S]*?<\/style>/g, "").replace(new RegExp(`\\n?\\s*<script\\b${keep}[\\s\\S]*?<\\/script>`, "g"), "").replaceAll('href="/styles.css"', 'href="styles.css"').replaceAll('href="/icons/', 'href="icons/');
4
- for (const s of ownScripts)
5
- out = out.replaceAll(`src="/${s}"`, `src="${s}"`);
6
- return out;
7
- }
1
+ export function sanitizeExtensionHtml(html,ownScripts){const keep=ownScripts.map((s)=>`(?![^>]*src="/?${s.replace(".","\\.")}")`).join("");let out=html.replace(/\n?<!-- stx SEO Tags -->[\s\S]*?(?=\n\s*<meta charset=)/,"").replace(/\n?\s*<style\b[\s\S]*?<\/style>/g,"").replace(new RegExp(`\\n?\\s*<script\\b${keep}[\\s\\S]*?<\\/script>`,"g"),"").replaceAll('href="/styles.css"','href="styles.css"').replaceAll('href="/icons/','href="icons/');for(const s of ownScripts)out=out.replaceAll(`src="/${s}"`,`src="${s}"`);return out}
package/dist/scaffold.js CHANGED
@@ -1,31 +1,15 @@
1
- import { existsSync } from "node:fs";
2
- import { mkdir } from "node:fs/promises";
3
- import { dirname, join } from "node:path";
4
- import process from "node:process";
5
- import { scaffoldSafariApp } from "./safari";
6
- export function resolveExtensionScaffoldTargets(target = "all") {
7
- return target === "all" ? ["chrome", "firefox", "safari"] : [target];
8
- }
9
- function safariAppName(name) {
10
- return name.replace(/[^A-Za-z0-9]/g, "") || "MyExtension";
11
- }
12
- function quotedList(values) {
13
- return `[${values.map((value) => `'${value}'`).join(", ")}]`;
14
- }
15
- function extensionConfigSource(config) {
16
- const safari = config.targets?.includes("safari") ? `
1
+ import{existsSync}from"node:fs";import{mkdir}from"node:fs/promises";import{dirname,join}from"node:path";import process from"node:process";import{scaffoldSafariApp}from"./safari";export function resolveExtensionScaffoldTargets(target="all"){return target==="all"?["chrome","firefox","safari"]:[target]}function safariAppName(name){return name.replace(/[^A-Za-z0-9]/g,"")||"MyExtension"}function quotedList(values){return`[${values.map((value)=>`'${value}'`).join(", ")}]`}function extensionConfigSource(config){const safari=config.targets?.includes("safari")?`
17
2
  safariBundleId: '${config.safariBundleId}',
18
- safariPlatforms: ${quotedList(config.safariPlatforms ?? ["macos", "ios"])},
19
- safariAppCategory: 'public.app-category.utilities',${config.safariTeamId ? `
20
- safariTeamId: '${config.safariTeamId}',` : ""}` : "", firefox = config.targets?.includes("firefox") ? `
3
+ safariPlatforms: ${quotedList(config.safariPlatforms??["macos","ios"])},
4
+ safariAppCategory: 'public.app-category.utilities',${config.safariTeamId?`
5
+ safariTeamId: '${config.safariTeamId}',`:""}`:"",firefox=config.targets?.includes("firefox")?`
21
6
  // Replace this before publishing to Mozilla Add-ons.
22
- geckoId: 'extension@example.com',` : "";
23
- return `import { defineExtension } from '@stacksjs/browser-extension'
7
+ geckoId: 'extension@example.com',`:"";return`import { defineExtension } from '@stacksjs/browser-extension'
24
8
 
25
9
  export default defineExtension({
26
10
  name: ${JSON.stringify(config.name)},
27
11
  description: 'A browser extension built with Stacks.',
28
- targets: ${quotedList(config.targets ?? [])},${firefox}${safari}
12
+ targets: ${quotedList(config.targets??[])},${firefox}${safari}
29
13
  background: 'src/background/index.ts',
30
14
  content: [
31
15
  { entry: 'src/content/index.ts', matches: ['<all_urls>'], runAt: 'document_start' },
@@ -38,55 +22,12 @@ export default defineExtension({
38
22
  permissions: ['storage', 'tabs'],
39
23
  },
40
24
  })
41
- `;
42
- }
43
- export async function scaffoldExtensionProject(options = {}) {
44
- const cwd = options.cwd ?? process.cwd(), name = options.name ?? "My Extension", targets = resolveExtensionScaffoldTargets(options.target), appName = safariAppName(name), config = {
45
- name,
46
- description: "A browser extension built with Stacks.",
47
- targets,
48
- background: "src/background/index.ts",
49
- content: [{ entry: "src/content/index.ts", matches: ["<all_urls>"], runAt: "document_start" }],
50
- pages: { popup: { template: "resources/views/popup.stx", script: "resources/scripts/popup.ts" } },
51
- public: "public",
52
- manifest: { permissions: ["storage", "tabs"] },
53
- ...targets.includes("firefox") && { geckoId: "extension@example.com" },
54
- ...targets.includes("safari") && {
55
- safariBundleId: options.bundleId ?? `com.example.${appName}`,
56
- safariTeamId: options.teamId,
57
- safariPlatforms: options.platforms ?? ["macos", "ios"],
58
- safariAppCategory: "public.app-category.utilities"
59
- }
60
- }, written = [], skipped = [], write = async (relative, content) => {
61
- const absolute = join(cwd, relative);
62
- if (existsSync(absolute) && !options.force) {
63
- skipped.push(relative);
64
- return;
65
- }
66
- await mkdir(dirname(absolute), { recursive: !0 });
67
- await Bun.write(absolute, content);
68
- written.push(relative);
69
- };
70
- await write("config/extension.ts", extensionConfigSource(config));
71
- await write("src/background/index.ts", `console.log(${JSON.stringify(`[${name}] background ready`)})
72
- `);
73
- await write("src/content/index.ts", `console.log(${JSON.stringify(`[${name}] content script loaded`)})
74
- `);
75
- await write("resources/scripts/popup.ts", `console.log(${JSON.stringify(`[${name}] popup ready`)})
76
- `);
77
- await write("resources/views/popup.stx", `<main class="popup">
25
+ `}export async function scaffoldExtensionProject(options={}){const cwd=options.cwd??process.cwd(),name=options.name??"My Extension",targets=resolveExtensionScaffoldTargets(options.target),appName=safariAppName(name),config={name,description:"A browser extension built with Stacks.",targets,background:"src/background/index.ts",content:[{entry:"src/content/index.ts",matches:["<all_urls>"],runAt:"document_start"}],pages:{popup:{template:"resources/views/popup.stx",script:"resources/scripts/popup.ts"}},public:"public",manifest:{permissions:["storage","tabs"]},...targets.includes("firefox")&&{geckoId:"extension@example.com"},...targets.includes("safari")&&{safariBundleId:options.bundleId??`com.example.${appName}`,safariTeamId:options.teamId,safariPlatforms:options.platforms??["macos","ios"],safariAppCategory:"public.app-category.utilities"}},written=[],skipped=[],write=async(relative,content)=>{const absolute=join(cwd,relative);if(existsSync(absolute)&&!options.force){skipped.push(relative);return}await mkdir(dirname(absolute),{recursive:!0});await Bun.write(absolute,content);written.push(relative)};await write("config/extension.ts",extensionConfigSource(config));await write("src/background/index.ts",`console.log(${JSON.stringify(`[${name}] background ready`)})
26
+ `);await write("src/content/index.ts",`console.log(${JSON.stringify(`[${name}] content script loaded`)})
27
+ `);await write("resources/scripts/popup.ts",`console.log(${JSON.stringify(`[${name}] popup ready`)})
28
+ `);await write("resources/views/popup.stx",`<main class="popup">
78
29
  <h1>${name}</h1>
79
30
  <p>Edit resources/views/popup.stx to build your popup.</p>
80
31
  <script src="/popup.js"></script>
81
32
  </main>
82
- `);
83
- await mkdir(join(cwd, "public"), { recursive: !0 });
84
- const safari = targets.includes("safari") ? await scaffoldSafariApp(config, {
85
- cwd,
86
- bundleId: config.safariBundleId,
87
- teamId: config.safariTeamId,
88
- version: options.version,
89
- force: options.force
90
- }) : void 0;
91
- return { config, written, skipped, safari };
92
- }
33
+ `);await mkdir(join(cwd,"public"),{recursive:!0});const safari=targets.includes("safari")?await scaffoldSafariApp(config,{cwd,bundleId:config.safariBundleId,teamId:config.safariTeamId,version:options.version,force:options.force}):void 0;return{config,written,skipped,safari}}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/browser-extension",
3
3
  "type": "module",
4
- "version": "0.70.257",
4
+ "version": "0.70.259",
5
5
  "description": "Build MV3 browser extensions (Chrome, Firefox, Safari) the Stacks way — manifest, content/background scripts, DNR rules, packaging, Safari container app, all config-driven.",
6
6
  "author": "Chris Breuer",
7
7
  "contributors": [
@@ -38,9 +38,16 @@
38
38
  "default": "./dist/index.js"
39
39
  },
40
40
  "./*": {
41
- "bun": "./dist/*",
42
- "import": "./dist/*",
43
- "default": "./dist/*"
41
+ "types": "./dist/*.d.ts",
42
+ "bun": "./dist/*.js",
43
+ "import": "./dist/*.js",
44
+ "default": "./dist/*.js"
45
+ },
46
+ "./*.js": {
47
+ "types": "./dist/*.d.ts",
48
+ "bun": "./dist/*.js",
49
+ "import": "./dist/*.js",
50
+ "default": "./dist/*.js"
44
51
  }
45
52
  },
46
53
  "module": "dist/index.js",