Skip to content

Commit 90113d0

Browse files
committed
fix(browser-extension): flatten Safari bundle resources
1 parent 08636f5 commit 90113d0

5 files changed

Lines changed: 121 additions & 6 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

storage/framework/core/browser-extension/safari-template/__APP_NAME__.xcodeproj/project.pbxproj

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
0C0000000000000000000011 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B0000000000000000000011 /* ContentView.swift */; };
1212
0C0000000000000000000012 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0B0000000000000000000012 /* Assets.xcassets */; };
1313
0C0000000000000000000020 /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B0000000000000000000020 /* SafariWebExtensionHandler.swift */; };
14-
0C0000000000000000000023 /* Resources in Resources */ = {isa = PBXBuildFile; fileRef = 0B0000000000000000000023 /* Resources */; };
1514
0C0000000000000000000030 /* __APP_NAME__ Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 0B0000000000000000000002 /* __APP_NAME__ Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
1615
/* End PBXBuildFile section */
1716

@@ -141,6 +140,7 @@
141140
0D0000000000000000000020 /* Sources */,
142141
0D0000000000000000000021 /* Frameworks */,
143142
0D0000000000000000000022 /* Resources */,
143+
0D0000000000000000000023 /* Copy Web Extension Resources */,
144144
);
145145
buildRules = (
146146
);
@@ -201,12 +201,31 @@
201201
isa = PBXResourcesBuildPhase;
202202
buildActionMask = 2147483647;
203203
files = (
204-
0C0000000000000000000023 /* Resources in Resources */,
205204
);
206205
runOnlyForDeploymentPostprocessing = 0;
207206
};
208207
/* End PBXResourcesBuildPhase section */
209208

209+
/* Begin PBXShellScriptBuildPhase section */
210+
0D0000000000000000000023 /* Copy Web Extension Resources */ = {
211+
isa = PBXShellScriptBuildPhase;
212+
alwaysOutOfDate = 1;
213+
buildActionMask = 2147483647;
214+
files = (
215+
);
216+
inputFileListPaths = (
217+
"$(SRCROOT)/__APP_NAME__ Extension/Resources.inputs.xcfilelist",
218+
);
219+
name = "Copy Web Extension Resources";
220+
outputFileListPaths = (
221+
"$(SRCROOT)/__APP_NAME__ Extension/Resources.outputs.xcfilelist",
222+
);
223+
runOnlyForDeploymentPostprocessing = 0;
224+
shellPath = /bin/sh;
225+
shellScript = "set -e\nresources_root=\"${SRCROOT}/__APP_NAME__ Extension/Resources/\"\nwhile IFS= read -r source_file; do\n relative_path=\"${source_file#$resources_root}\"\n output_file=\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/${relative_path}\"\n /bin/mkdir -p \"$(/usr/bin/dirname \"$output_file\")\"\n /bin/cp \"$source_file\" \"$output_file\"\ndone < \"$SCRIPT_INPUT_FILE_LIST_0\"\n";
226+
};
227+
/* End PBXShellScriptBuildPhase section */
228+
210229
/* Begin PBXSourcesBuildPhase section */
211230
0D0000000000000000000010 /* Sources */ = {
212231
isa = PBXSourcesBuildPhase;

storage/framework/core/browser-extension/src/safari.ts

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,55 @@ function fillTemplate(text: string, vars: ScaffoldVars): string {
5959
.replaceAll('__YEAR__', String(new Date().getFullYear()))
6060
}
6161

62+
function safariResourceCopyBuildPhase(appName: string): string {
63+
return `/* Begin PBXShellScriptBuildPhase section */
64+
\t\t0D0000000000000000000023 /* Copy Web Extension Resources */ = {
65+
\t\t\tisa = PBXShellScriptBuildPhase;
66+
\t\t\talwaysOutOfDate = 1;
67+
\t\t\tbuildActionMask = 2147483647;
68+
\t\t\tfiles = (
69+
\t\t\t);
70+
\t\t\tinputFileListPaths = (
71+
\t\t\t\t"$(SRCROOT)/${appName} Extension/Resources.inputs.xcfilelist",
72+
\t\t\t);
73+
\t\t\tname = "Copy Web Extension Resources";
74+
\t\t\toutputFileListPaths = (
75+
\t\t\t\t"$(SRCROOT)/${appName} Extension/Resources.outputs.xcfilelist",
76+
\t\t\t);
77+
\t\t\trunOnlyForDeploymentPostprocessing = 0;
78+
\t\t\tshellPath = /bin/sh;
79+
\t\t\tshellScript = "set -e\\nresources_root=\\"\${SRCROOT}/${appName} Extension/Resources/\\"\\nwhile IFS= read -r source_file; do\\n relative_path=\\"\${source_file#$resources_root}\\"\\n output_file=\\"\${TARGET_BUILD_DIR}/\${UNLOCALIZED_RESOURCES_FOLDER_PATH}/\${relative_path}\\"\\n /bin/mkdir -p \\"$(/usr/bin/dirname \\\"$output_file\\\")\\"\\n /bin/cp \\"$source_file\\" \\"$output_file\\"\\ndone < \\"$SCRIPT_INPUT_FILE_LIST_0\\"\\n";
80+
\t\t};
81+
/* End PBXShellScriptBuildPhase section */
82+
83+
`
84+
}
85+
86+
/** Upgrade a Stacks-generated project that copied Resources as a nested folder. */
87+
export function migrateSafariResourceBuildPhase(project: string, appName: string): string {
88+
if (project.includes('SCRIPT_INPUT_FILE_LIST_0'))
89+
return project
90+
if (project.includes('0D0000000000000000000023 /* Copy Web Extension Resources */')) {
91+
return project.replace(
92+
/\/\* Begin PBXShellScriptBuildPhase section \*\/[\s\S]*?\/\* End PBXShellScriptBuildPhase section \*\/\n/,
93+
safariResourceCopyBuildPhase(appName),
94+
)
95+
}
96+
if (!project.includes('0C0000000000000000000023 /* Resources in Resources */'))
97+
return project
98+
99+
return project
100+
.replace(/^\s*0C0000000000000000000023 \/\* Resources in Resources \*\/.*\n/m, '')
101+
.replace(/^\s*0C0000000000000000000023 \/\* Resources in Resources \*\/,\n/mg, '')
102+
.replace(
103+
'\t\t\t\t0D0000000000000000000022 /* Resources */,\n',
104+
'\t\t\t\t0D0000000000000000000022 /* Resources */,\n\t\t\t\t0D0000000000000000000023 /* Copy Web Extension Resources */,\n',
105+
)
106+
.replace('/* Begin PBXSourcesBuildPhase section */', `${safariResourceCopyBuildPhase(appName)}/* Begin PBXSourcesBuildPhase section */`)
107+
}
108+
62109
function* walk(dir: string, prefix = ''): Generator<string> {
63-
for (const entry of readdirSync(dir)) {
110+
for (const entry of readdirSync(dir).sort()) {
64111
const rel = prefix ? `${prefix}/${entry}` : entry
65112
if (statSync(join(dir, entry)).isDirectory()) yield* walk(join(dir, entry), rel)
66113
else yield rel
@@ -202,7 +249,16 @@ export async function syncSafariResources(config: ExtensionConfig, options: Safa
202249
throw new Error(`[browser-extension] ${outdir}/manifest.json is missing. Run extension:build --target safari first.`)
203250

204251
const appName = safariAppName(config)
205-
const resources = join(safariProjectDir(cwd, options.dir), `${appName} Extension`, 'Resources')
252+
const projectDir = safariProjectDir(cwd, options.dir)
253+
const projectPath = join(projectDir, `${appName}.xcodeproj`, 'project.pbxproj')
254+
if (existsSync(projectPath)) {
255+
const project = await Bun.file(projectPath).text()
256+
const migrated = migrateSafariResourceBuildPhase(project, appName)
257+
if (migrated !== project)
258+
await Bun.write(projectPath, migrated)
259+
}
260+
261+
const resources = join(projectDir, `${appName} Extension`, 'Resources')
206262
const exclude = new Set(config.safariExclude ?? [])
207263

208264
if (existsSync(resources)) {
@@ -214,15 +270,22 @@ export async function syncSafariResources(config: ExtensionConfig, options: Safa
214270
await mkdir(resources, { recursive: true })
215271

216272
let files = 0
273+
const synced: string[] = []
217274
for (const rel of walk(outdir)) {
218-
if (exclude.has(rel))
275+
if (exclude.has(rel) || rel.split('/').includes('.DS_Store'))
219276
continue
220277
const dest = join(resources, rel)
221278
await mkdir(dirname(dest), { recursive: true })
222279
cpSync(join(outdir, rel), dest)
280+
synced.push(rel)
223281
files += 1
224282
}
225283

284+
const inputs = synced.map(rel => join(resources, rel)).join('\n')
285+
const outputs = synced.map(rel => `$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/${rel}`).join('\n')
286+
await Bun.write(join(projectDir, `${appName} Extension`, 'Resources.inputs.xcfilelist'), `${inputs}\n`)
287+
await Bun.write(join(projectDir, `${appName} Extension`, 'Resources.outputs.xcfilelist'), `${outputs}\n`)
288+
226289
return { resources, files }
227290
}
228291

storage/framework/core/browser-extension/tests/safari.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { join } from 'node:path'
66
import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
77
import { resolveOutdir, rewriteBrowserNamespace } from '../src/build'
88
import { generateManifest } from '../src/manifest'
9-
import { scaffoldSafariApp, safariAppName, syncSafariResources } from '../src/safari'
9+
import { migrateSafariResourceBuildPhase, scaffoldSafariApp, safariAppName, syncSafariResources } from '../src/safari'
1010

1111
const config: ExtensionConfig = {
1212
name: 'Test Extension',
@@ -117,6 +117,10 @@ describe('safari scaffold + sync', () => {
117117
expect(pbxproj).toContain('MARKETING_VERSION = 1.2.3;')
118118
expect(pbxproj).toContain('DEVELOPMENT_TEAM = "TEAM123456";')
119119
expect(pbxproj).not.toContain('__APP_NAME__')
120+
expect(pbxproj).toContain('Copy Web Extension Resources')
121+
expect(pbxproj).toContain('Resources.inputs.xcfilelist')
122+
expect(pbxproj).toContain('Resources.outputs.xcfilelist')
123+
expect(pbxproj).not.toContain('Resources in Resources')
120124

121125
const appPlist = await Bun.file(join(dir, 'TestExtension', 'Info.plist')).text()
122126
expect(appPlist).toContain('<string>Test Extension</string>')
@@ -168,6 +172,19 @@ describe('safari scaffold + sync', () => {
168172
expect(existsSync(join(resources, 'rules', 'static.json'))).toBe(true)
169173
expect(existsSync(join(resources, 'marketing.html'))).toBe(false)
170174
expect(existsSync(join(resources, 'marketing.js'))).toBe(false)
175+
176+
const inputs = await Bun.file(join(cwd, 'safari', 'TestExtension Extension', 'Resources.inputs.xcfilelist')).text()
177+
const outputs = await Bun.file(join(cwd, 'safari', 'TestExtension Extension', 'Resources.outputs.xcfilelist')).text()
178+
expect(inputs.trim().split('\n')).toEqual([
179+
join(resources, 'background.js'),
180+
join(resources, 'manifest.json'),
181+
join(resources, 'rules', 'static.json'),
182+
])
183+
expect(outputs.trim().split('\n')).toEqual([
184+
'$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/background.js',
185+
'$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/manifest.json',
186+
'$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/rules/static.json',
187+
])
171188
})
172189

173190
it('sync clears stale files from previous runs', async () => {
@@ -181,4 +198,18 @@ describe('safari scaffold + sync', () => {
181198
expect(existsSync(join(resources, 'stale.js'))).toBe(false)
182199
expect(existsSync(join(resources, 'manifest.json'))).toBe(true)
183200
})
201+
202+
it('migrates legacy projects that nested the web extension resources', () => {
203+
const legacy = `
204+
0C0000000000000000000023 /* Resources in Resources */ = {isa = PBXBuildFile; fileRef = 0B0000000000000000000023 /* Resources */; };
205+
\t\t\t\t0D0000000000000000000022 /* Resources */,
206+
\t\t\t\t0C0000000000000000000023 /* Resources in Resources */,
207+
/* Begin PBXSourcesBuildPhase section */
208+
`
209+
const migrated = migrateSafariResourceBuildPhase(legacy, 'TestExtension')
210+
expect(migrated).toContain('Copy Web Extension Resources')
211+
expect(migrated).toContain('Resources.inputs.xcfilelist')
212+
expect(migrated).toContain('Resources.outputs.xcfilelist')
213+
expect(migrated).not.toContain('Resources in Resources')
214+
})
184215
})

0 commit comments

Comments
 (0)