Skip to content

Commit da8fec7

Browse files
support bundle folder in the bundle ui step config
1 parent 01a720b commit da8fec7

6 files changed

Lines changed: 109 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
@@ -84,12 +84,17 @@ const uiExtensionSpec = createExtensionSpecification({
8484
dependency,
8585
schema: UIExtensionSchema,
8686
buildConfig: {mode: 'ui'},
87-
getOutputRelativePath: (extension: ExtensionInstance<UIExtensionConfigType>) => `dist/${extension.handle}.js`,
87+
getOutputRelativePath: (extension: ExtensionInstance<UIExtensionConfigType>) => `${extension.handle}.js`,
8888
clientSteps: [
8989
{
9090
lifecycle: 'deploy',
9191
steps: [
92-
{id: 'bundle-ui', name: 'Bundle UI Extension', type: 'bundle_ui', config: {generatesAssetsManifest: true}},
92+
{
93+
id: 'bundle-ui',
94+
name: 'Bundle UI Extension',
95+
type: 'bundle_ui',
96+
config: {generatesAssetsManifest: true, bundleFolder: 'dist/'},
97+
},
9398
{
9499
id: 'include-ui-extension-assets',
95100
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: 12 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,9 +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)
25+
const bundleOutputDir = step.config?.bundleFolder
26+
? joinPath(dirname(context.extension.outputPath), step.config.bundleFolder)
27+
: dirname(context.extension.outputPath)
2428
// Copy the locally built files into the bundle
25-
await copyFile(dirname(localOutputPath), dirname(context.extension.outputPath))
29+
await copyFile(dirname(localOutputPath), bundleOutputDir)
2630

2731
if (!step.config?.generatesAssetsManifest) return
2832

@@ -32,7 +36,7 @@ export async function executeBundleUIStep(step: BundleUIStep, context: BuildCont
3236
(ep): ep is ExtensionPointWithBuildManifest => typeof ep === 'object' && ep.build_manifest,
3337
)
3438

35-
const entries = extractBuiltAssetEntries(pointsWithManifest)
39+
const entries = extractBuiltAssetEntries(pointsWithManifest, step.config?.bundleFolder)
3640
if (Object.keys(entries).length > 0) {
3741
await createOrUpdateManifestFile(context, entries)
3842
}
@@ -42,15 +46,18 @@ export async function executeBundleUIStep(step: BundleUIStep, context: BuildCont
4246
* Extracts built asset filepaths from `build_manifest` on each extension point,
4347
* grouped by target. Returns a map of target → `{assetName: filepath}`.
4448
*/
45-
function extractBuiltAssetEntries(extensionPoints: {target: string; build_manifest: BuildManifest}[]): {
49+
function extractBuiltAssetEntries(
50+
extensionPoints: {target: string; build_manifest: BuildManifest}[],
51+
bundleFolder?: string,
52+
): {
4653
[target: string]: {[assetName: string]: string}
4754
} {
4855
const entries: {[target: string]: {[assetName: string]: string}} = {}
4956
for (const {target, build_manifest: buildManifest} of extensionPoints) {
5057
if (!buildManifest?.assets) continue
5158
const assets: {[name: string]: string} = {}
5259
for (const [name, asset] of Object.entries(buildManifest.assets)) {
53-
if (asset?.filepath) assets[name] = asset.filepath
60+
if (asset?.filepath) assets[name] = bundleFolder ? joinPath(bundleFolder, asset.filepath) : asset.filepath
5461
}
5562
if (Object.keys(assets).length > 0) entries[target] = assets
5663
}

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

Lines changed: 50 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,55 @@ 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+
{CUSTOM_EXTENSION_POINT: {main: 'dist/test-ui-extension.js', should_render: 'dist/test-ui-extension-conditions.js'}},
279+
{},
280+
)
281+
282+
const got = await getUIExtensionPayload(uiExtension, tmpDir, {
283+
...createMockOptions(tmpDir, [uiExtension]),
284+
currentDevelopmentPayload: {hidden: true, status: 'success'},
285+
})
286+
287+
expect(got.extensionPoints).toMatchObject([
288+
{
289+
target: 'CUSTOM_EXTENSION_POINT',
290+
assets: {
291+
main: {
292+
name: 'main',
293+
url: 'http://tunnel-url.com/extensions/devUUID/assets/dist/test-ui-extension.js',
294+
lastUpdated: expect.any(Number),
295+
},
296+
should_render: {
297+
name: 'should_render',
298+
url: 'http://tunnel-url.com/extensions/devUUID/assets/dist/test-ui-extension-conditions.js',
299+
lastUpdated: expect.any(Number),
300+
},
301+
},
302+
},
303+
])
304+
})
305+
})
306+
271307
test('maps intents from manifest.json to asset payloads', async () => {
272308
await inTemporaryDirectory(async (tmpDir) => {
273309
const uiExtension = await testUIExtension({

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

Lines changed: 36 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
}
@@ -206,6 +211,7 @@ async function intentsAssetMapper({
206211
const intents = await Promise.all(
207212
extensionPoint.intents.map(async (intent) => ({
208213
...intent,
214+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
209215
schema: await getAssetPayload('schema', intent.schema as string, url, extension),
210216
})),
211217
)
@@ -215,12 +221,29 @@ async function intentsAssetMapper({
215221

216222
type AssetMapper = (context: AssetMapperContext) => Promise<Partial<DevNewExtensionPointSchema>>
217223

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

226249
/**
@@ -236,7 +259,13 @@ async function mapManifestAssetsToPayload(
236259
): Promise<Partial<DevNewExtensionPointSchema>> {
237260
const mappingResults = await Promise.all(
238261
Object.keys(manifestEntry).map(async (identifier) => {
239-
const context: AssetMapperContext = {identifier, extensionPoint, url, extension}
262+
const context: AssetMapperContext = {
263+
identifier,
264+
extensionPoint,
265+
url,
266+
extension,
267+
manifestValue: manifestEntry[identifier],
268+
}
240269
return ASSET_MAPPERS[identifier]?.(context) ?? defaultAssetMapper(context)
241270
}),
242271
)

0 commit comments

Comments
 (0)