Skip to content

Commit 6ca0426

Browse files
Merge pull request #7340 from Shopify/04-17-support_bundle_folder_in_the_bundle_ui_step_config
support bundle folder in the bundle ui step config
2 parents 72d989f + ffa8ebe commit 6ca0426

6 files changed

Lines changed: 112 additions & 29 deletions

File tree

packages/app/src/cli/models/extensions/specifications/ui_extension.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,17 @@ const uiExtensionSpec = createExtensionSpecification({
8383
identifier: 'ui_extension',
8484
dependency,
8585
schema: UIExtensionSchema,
86-
getOutputRelativePath: (extension: ExtensionInstance<UIExtensionConfigType>) => `dist/${extension.handle}.js`,
86+
getOutputRelativePath: (extension: ExtensionInstance<UIExtensionConfigType>) => `${extension.handle}.js`,
8787
clientSteps: [
8888
{
8989
lifecycle: 'deploy',
9090
steps: [
91-
{id: 'bundle-ui', name: 'Bundle UI Extension', type: 'bundle_ui', config: {generatesAssetsManifest: true}},
91+
{
92+
id: 'bundle-ui',
93+
name: 'Bundle UI Extension',
94+
type: 'bundle_ui',
95+
config: {generatesAssetsManifest: true, bundleFolder: 'dist/'},
96+
},
9297
{
9398
id: 'include-ui-extension-assets',
9499
name: 'Include UI Extension Assets',

packages/app/src/cli/services/build/client-steps.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface BundleUIStep extends BaseStep {
2626
readonly type: 'bundle_ui'
2727
readonly config?: {
2828
readonly generatesAssetsManifest?: boolean
29+
readonly bundleFolder?: string
2930
}
3031
}
3132

packages/app/src/cli/services/build/extension.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,10 @@ export async function buildUIExtension(extension: ExtensionInstance, options: Ex
6767
env.APP_URL = options.appURL
6868
}
6969

70+
const buildDirectory = options.buildDirectory ?? ''
71+
7072
// Always build into the extension's local directory (e.g. ext/dist/handle.js)
71-
const localOutputPath = joinPath(extension.directory, extension.outputRelativePath)
73+
const localOutputPath = joinPath(extension.directory, buildDirectory, extension.outputRelativePath)
7274

7375
const {main, assets} = extension.getBundleExtensionStdinContent()
7476

packages/app/src/cli/services/build/steps/bundle-ui-step.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {createOrUpdateManifestFile} from './include-assets/generate-manifest.js'
22
import {buildUIExtension} from '../extension.js'
33
import {BuildManifest} from '../../../models/extensions/specifications/ui_extension.js'
44
import {copyFile} from '@shopify/cli-kit/node/fs'
5-
import {dirname} from '@shopify/cli-kit/node/path'
5+
import {dirname, joinPath} from '@shopify/cli-kit/node/path'
66
import type {BundleUIStep, BuildContext} from '../client-steps.js'
77

88
interface ExtensionPointWithBuildManifest {
@@ -20,10 +20,13 @@ interface ExtensionPointWithBuildManifest {
2020
*/
2121
export async function executeBundleUIStep(step: BundleUIStep, context: BuildContext): Promise<void> {
2222
const config = context.extension.configuration
23+
context.options.buildDirectory = step.config?.bundleFolder ?? undefined
2324
const localOutputPath = await buildUIExtension(context.extension, context.options)
2425
// When invoked outside a bundle directory (e.g. `shopify app build`), localOutputPath and outputPath collapse onto the same directory; fs-extra rejects same-path copies.
2526
const localOutputDir = dirname(localOutputPath)
26-
const bundleOutputDir = dirname(context.extension.outputPath)
27+
const bundleOutputDir = step.config?.bundleFolder
28+
? joinPath(dirname(context.extension.outputPath), step.config.bundleFolder)
29+
: dirname(context.extension.outputPath)
2730
if (localOutputDir !== bundleOutputDir) {
2831
await copyFile(localOutputDir, bundleOutputDir)
2932
}
@@ -36,7 +39,7 @@ export async function executeBundleUIStep(step: BundleUIStep, context: BuildCont
3639
(ep): ep is ExtensionPointWithBuildManifest => typeof ep === 'object' && ep.build_manifest,
3740
)
3841

39-
const entries = extractBuiltAssetEntries(pointsWithManifest)
42+
const entries = extractBuiltAssetEntries(pointsWithManifest, step.config?.bundleFolder)
4043
if (Object.keys(entries).length > 0) {
4144
await createOrUpdateManifestFile(context, entries)
4245
}
@@ -46,15 +49,18 @@ export async function executeBundleUIStep(step: BundleUIStep, context: BuildCont
4649
* Extracts built asset filepaths from `build_manifest` on each extension point,
4750
* grouped by target. Returns a map of target → `{assetName: filepath}`.
4851
*/
49-
function extractBuiltAssetEntries(extensionPoints: {target: string; build_manifest: BuildManifest}[]): {
52+
function extractBuiltAssetEntries(
53+
extensionPoints: {target: string; build_manifest: BuildManifest}[],
54+
bundleFolder?: string,
55+
): {
5056
[target: string]: {[assetName: string]: string}
5157
} {
5258
const entries: {[target: string]: {[assetName: string]: string}} = {}
5359
for (const {target, build_manifest: buildManifest} of extensionPoints) {
5460
if (!buildManifest?.assets) continue
5561
const assets: {[name: string]: string} = {}
5662
for (const [name, asset] of Object.entries(buildManifest.assets)) {
57-
if (asset?.filepath) assets[name] = asset.filepath
63+
if (asset?.filepath) assets[name] = bundleFolder ? joinPath(bundleFolder, asset.filepath) : asset.filepath
5864
}
5965
if (Object.keys(assets).length > 0) entries[target] = assets
6066
}

packages/app/src/cli/services/dev/extension/payload.test.ts

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -206,15 +206,8 @@ describe('getUIExtensionPayload', () => {
206206
})
207207
})
208208

209-
test('maps main and should_render from build_manifest', async () => {
209+
test('maps main and should_render paths from manifest.json', async () => {
210210
await inTemporaryDirectory(async (tmpDir) => {
211-
const buildManifest = {
212-
assets: {
213-
main: {module: './src/ExtensionPointA.js', filepath: 'test-ui-extension.js'},
214-
should_render: {module: './src/ShouldRender.js', filepath: 'test-ui-extension-conditions.js'},
215-
},
216-
}
217-
218211
const uiExtension = await testUIExtension({
219212
directory: tmpDir,
220213
configuration: {
@@ -224,18 +217,12 @@ describe('getUIExtensionPayload', () => {
224217
{
225218
target: 'CUSTOM_EXTENSION_POINT',
226219
module: './src/ExtensionPointA.js',
227-
build_manifest: buildManifest,
228220
},
229221
],
230222
},
231223
devUUID: 'devUUID',
232224
})
233225

234-
// Create source files so lastUpdated resolves
235-
await mkdir(joinPath(tmpDir, 'src'))
236-
await writeFile(joinPath(tmpDir, 'src', 'ExtensionPointA.js'), '// main')
237-
await writeFile(joinPath(tmpDir, 'src', 'ShouldRender.js'), '// should render')
238-
239226
await setupBuildOutput(
240227
uiExtension,
241228
tmpDir,
@@ -268,6 +255,60 @@ describe('getUIExtensionPayload', () => {
268255
})
269256
})
270257

258+
test('maps main and should_render with bundleFolder prefix from manifest.json', async () => {
259+
await inTemporaryDirectory(async (tmpDir) => {
260+
const uiExtension = await testUIExtension({
261+
directory: tmpDir,
262+
configuration: {
263+
name: 'test-ui-extension',
264+
type: 'ui_extension',
265+
extension_points: [
266+
{
267+
target: 'CUSTOM_EXTENSION_POINT',
268+
module: './src/ExtensionPointA.js',
269+
},
270+
],
271+
},
272+
devUUID: 'devUUID',
273+
})
274+
275+
await setupBuildOutput(
276+
uiExtension,
277+
tmpDir,
278+
{
279+
CUSTOM_EXTENSION_POINT: {
280+
main: 'dist/test-ui-extension.js',
281+
should_render: 'dist/test-ui-extension-conditions.js',
282+
},
283+
},
284+
{},
285+
)
286+
287+
const got = await getUIExtensionPayload(uiExtension, tmpDir, {
288+
...createMockOptions(tmpDir, [uiExtension]),
289+
currentDevelopmentPayload: {hidden: true, status: 'success'},
290+
})
291+
292+
expect(got.extensionPoints).toMatchObject([
293+
{
294+
target: 'CUSTOM_EXTENSION_POINT',
295+
assets: {
296+
main: {
297+
name: 'main',
298+
url: 'http://tunnel-url.com/extensions/devUUID/assets/dist/test-ui-extension.js',
299+
lastUpdated: expect.any(Number),
300+
},
301+
should_render: {
302+
name: 'should_render',
303+
url: 'http://tunnel-url.com/extensions/devUUID/assets/dist/test-ui-extension-conditions.js',
304+
lastUpdated: expect.any(Number),
305+
},
306+
},
307+
},
308+
])
309+
})
310+
})
311+
271312
test('maps intents from manifest.json to asset payloads', async () => {
272313
await inTemporaryDirectory(async (tmpDir) => {
273314
const uiExtension = await testUIExtension({

packages/app/src/cli/services/dev/extension/payload.ts

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ interface AssetMapperContext {
1919
extensionPoint: DevNewExtensionPointSchema
2020
url: string
2121
extension: ExtensionInstance
22+
manifestValue?: unknown
2223
}
2324

2425
export async function getUIExtensionPayload(
@@ -47,11 +48,14 @@ export async function getUIExtensionPayload(
4748

4849
const defaultConfig = {
4950
assets: {
50-
main: {
51-
name: 'main',
52-
url: `${url}/assets/${extension.outputFileName}`,
53-
lastUpdated: (await fileLastUpdatedTimestamp(extensionOutputPath)) ?? 0,
54-
},
51+
main:
52+
isNewExtensionPointsSchema(extensionPoints) && extensionPoints[0]?.assets?.main
53+
? extensionPoints[0].assets.main
54+
: {
55+
name: 'main',
56+
url: `${url}/assets/${extension.outputFileName}`,
57+
lastUpdated: (await fileLastUpdatedTimestamp(extensionOutputPath)) ?? 0,
58+
},
5559
},
5660
supportedFeatures: {
5761
runsOffline: extension.configuration.supported_features?.runs_offline ?? false,
@@ -134,10 +138,11 @@ async function getExtensionPoints(extension: ExtensionInstance, url: string, bui
134138
return payload
135139
}
136140

137-
return {
141+
const payloadWithAssets = {
138142
...payload,
139143
...(await mapManifestAssetsToPayload(manifestEntry, extensionPoint, url, extension)),
140144
}
145+
return payloadWithAssets
141146
}),
142147
)
143148
}
@@ -215,12 +220,29 @@ async function intentsAssetMapper({
215220

216221
type AssetMapper = (context: AssetMapperContext) => Promise<Partial<DevNewExtensionPointSchema>>
217222

223+
/**
224+
* Mapper for compiled built assets (main, should_render).
225+
* Reads the filepath directly from manifest.json so the bundleFolder prefix is preserved.
226+
*/
227+
async function builtAssetMapper({
228+
identifier,
229+
manifestValue,
230+
url,
231+
extension,
232+
}: AssetMapperContext): Promise<Partial<DevNewExtensionPointSchema>> {
233+
if (typeof manifestValue !== 'string') return {}
234+
const payload = await getAssetPayload(identifier, manifestValue, url, extension)
235+
return {assets: {[payload.name]: payload}}
236+
}
237+
218238
/**
219239
* Asset mappers registry - defines how each asset type should be handled.
220240
* Assets not in this registry use the defaultAssetMapper.
221241
*/
222242
const ASSET_MAPPERS: {[key: string]: AssetMapper | undefined} = {
223243
intents: intentsAssetMapper,
244+
main: builtAssetMapper,
245+
should_render: builtAssetMapper,
224246
}
225247

226248
/**
@@ -236,7 +258,13 @@ async function mapManifestAssetsToPayload(
236258
): Promise<Partial<DevNewExtensionPointSchema>> {
237259
const mappingResults = await Promise.all(
238260
Object.keys(manifestEntry).map(async (identifier) => {
239-
const context: AssetMapperContext = {identifier, extensionPoint, url, extension}
261+
const context: AssetMapperContext = {
262+
identifier,
263+
extensionPoint,
264+
url,
265+
extension,
266+
manifestValue: manifestEntry[identifier],
267+
}
240268
return ASSET_MAPPERS[identifier]?.(context) ?? defaultAssetMapper(context)
241269
}),
242270
)

0 commit comments

Comments
 (0)