Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,17 @@ const uiExtensionSpec = createExtensionSpecification({
dependency,
schema: UIExtensionSchema,
buildConfig: {mode: 'ui'},
getOutputRelativePath: (extension: ExtensionInstance<UIExtensionConfigType>) => `dist/${extension.handle}.js`,
getOutputRelativePath: (extension: ExtensionInstance<UIExtensionConfigType>) => `${extension.handle}.js`,
clientSteps: [
{
lifecycle: 'deploy',
steps: [
{id: 'bundle-ui', name: 'Bundle UI Extension', type: 'bundle_ui', config: {generatesAssetsManifest: true}},
{
id: 'bundle-ui',
name: 'Bundle UI Extension',
type: 'bundle_ui',
config: {generatesAssetsManifest: true, bundleFolder: 'dist/'},
},
{
id: 'include-ui-extension-assets',
name: 'Include UI Extension Assets',
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/cli/services/build/client-steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface BundleUIStep extends BaseStep {
readonly type: 'bundle_ui'
readonly config?: {
readonly generatesAssetsManifest?: boolean
readonly bundleFolder?: string
}
}

Expand Down
4 changes: 3 additions & 1 deletion packages/app/src/cli/services/build/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,10 @@ export async function buildUIExtension(extension: ExtensionInstance, options: Ex
env.APP_URL = options.appURL
}

const buildDirectory = options.buildDirectory ?? ''

// Always build into the extension's local directory (e.g. ext/dist/handle.js)
const localOutputPath = joinPath(extension.directory, extension.outputRelativePath)
const localOutputPath = joinPath(extension.directory, buildDirectory, extension.outputRelativePath)

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

Expand Down
17 changes: 12 additions & 5 deletions packages/app/src/cli/services/build/steps/bundle-ui-step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {createOrUpdateManifestFile} from './include-assets/generate-manifest.js'
import {buildUIExtension} from '../extension.js'
import {BuildManifest} from '../../../models/extensions/specifications/ui_extension.js'
import {copyFile} from '@shopify/cli-kit/node/fs'
import {dirname} from '@shopify/cli-kit/node/path'
import {dirname, joinPath} from '@shopify/cli-kit/node/path'
import type {BundleUIStep, BuildContext} from '../client-steps.js'

interface ExtensionPointWithBuildManifest {
Expand All @@ -20,9 +20,13 @@ interface ExtensionPointWithBuildManifest {
*/
export async function executeBundleUIStep(step: BundleUIStep, context: BuildContext): Promise<void> {
const config = context.extension.configuration
context.options.buildDirectory = step.config?.bundleFolder ?? undefined
const localOutputPath = await buildUIExtension(context.extension, context.options)
const bundleOutputDir = step.config?.bundleFolder
? joinPath(dirname(context.extension.outputPath), step.config.bundleFolder)
: dirname(context.extension.outputPath)
// Copy the locally built files into the bundle
await copyFile(dirname(localOutputPath), dirname(context.extension.outputPath))
await copyFile(dirname(localOutputPath), bundleOutputDir)

if (!step.config?.generatesAssetsManifest) return

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

const entries = extractBuiltAssetEntries(pointsWithManifest)
const entries = extractBuiltAssetEntries(pointsWithManifest, step.config?.bundleFolder)
if (Object.keys(entries).length > 0) {
await createOrUpdateManifestFile(context, entries)
}
Expand All @@ -42,15 +46,18 @@ export async function executeBundleUIStep(step: BundleUIStep, context: BuildCont
* Extracts built asset filepaths from `build_manifest` on each extension point,
* grouped by target. Returns a map of target → `{assetName: filepath}`.
*/
function extractBuiltAssetEntries(extensionPoints: {target: string; build_manifest: BuildManifest}[]): {
function extractBuiltAssetEntries(
extensionPoints: {target: string; build_manifest: BuildManifest}[],
bundleFolder?: string,
): {
[target: string]: {[assetName: string]: string}
} {
const entries: {[target: string]: {[assetName: string]: string}} = {}
for (const {target, build_manifest: buildManifest} of extensionPoints) {
if (!buildManifest?.assets) continue
const assets: {[name: string]: string} = {}
for (const [name, asset] of Object.entries(buildManifest.assets)) {
if (asset?.filepath) assets[name] = asset.filepath
if (asset?.filepath) assets[name] = bundleFolder ? joinPath(bundleFolder, asset.filepath) : asset.filepath
}
if (Object.keys(assets).length > 0) entries[target] = assets
}
Expand Down
64 changes: 50 additions & 14 deletions packages/app/src/cli/services/dev/extension/payload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,15 +206,8 @@ describe('getUIExtensionPayload', () => {
})
})

test('maps main and should_render from build_manifest', async () => {
test('maps main and should_render paths from manifest.json', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const buildManifest = {
assets: {
main: {module: './src/ExtensionPointA.js', filepath: 'test-ui-extension.js'},
should_render: {module: './src/ShouldRender.js', filepath: 'test-ui-extension-conditions.js'},
},
}

const uiExtension = await testUIExtension({
directory: tmpDir,
configuration: {
Expand All @@ -224,18 +217,12 @@ describe('getUIExtensionPayload', () => {
{
target: 'CUSTOM_EXTENSION_POINT',
module: './src/ExtensionPointA.js',
build_manifest: buildManifest,
},
],
},
devUUID: 'devUUID',
})

// Create source files so lastUpdated resolves
await mkdir(joinPath(tmpDir, 'src'))
await writeFile(joinPath(tmpDir, 'src', 'ExtensionPointA.js'), '// main')
await writeFile(joinPath(tmpDir, 'src', 'ShouldRender.js'), '// should render')

await setupBuildOutput(
uiExtension,
tmpDir,
Expand Down Expand Up @@ -268,6 +255,55 @@ describe('getUIExtensionPayload', () => {
})
})

test('maps main and should_render with bundleFolder prefix from manifest.json', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const uiExtension = await testUIExtension({
directory: tmpDir,
configuration: {
name: 'test-ui-extension',
type: 'ui_extension',
extension_points: [
{
target: 'CUSTOM_EXTENSION_POINT',
module: './src/ExtensionPointA.js',
},
],
},
devUUID: 'devUUID',
})

await setupBuildOutput(
uiExtension,
tmpDir,
{CUSTOM_EXTENSION_POINT: {main: 'dist/test-ui-extension.js', should_render: 'dist/test-ui-extension-conditions.js'}},
{},
)

const got = await getUIExtensionPayload(uiExtension, tmpDir, {
...createMockOptions(tmpDir, [uiExtension]),
currentDevelopmentPayload: {hidden: true, status: 'success'},
})

expect(got.extensionPoints).toMatchObject([
{
target: 'CUSTOM_EXTENSION_POINT',
assets: {
main: {
name: 'main',
url: 'http://tunnel-url.com/extensions/devUUID/assets/dist/test-ui-extension.js',
lastUpdated: expect.any(Number),
},
should_render: {
name: 'should_render',
url: 'http://tunnel-url.com/extensions/devUUID/assets/dist/test-ui-extension-conditions.js',
lastUpdated: expect.any(Number),
},
},
},
])
})
})

test('maps intents from manifest.json to asset payloads', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const uiExtension = await testUIExtension({
Expand Down
43 changes: 36 additions & 7 deletions packages/app/src/cli/services/dev/extension/payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface AssetMapperContext {
extensionPoint: DevNewExtensionPointSchema
url: string
extension: ExtensionInstance
manifestValue?: unknown
}

export async function getUIExtensionPayload(
Expand Down Expand Up @@ -47,11 +48,14 @@ export async function getUIExtensionPayload(

const defaultConfig = {
assets: {
main: {
name: 'main',
url: `${url}/assets/${extension.outputFileName}`,
lastUpdated: (await fileLastUpdatedTimestamp(extensionOutputPath)) ?? 0,
},
main:
isNewExtensionPointsSchema(extensionPoints) && extensionPoints[0]?.assets?.main
? extensionPoints[0].assets.main
: {
name: 'main',
url: `${url}/assets/${extension.outputFileName}`,
lastUpdated: (await fileLastUpdatedTimestamp(extensionOutputPath)) ?? 0,
},
},
supportedFeatures: {
runsOffline: extension.configuration.supported_features?.runs_offline ?? false,
Expand Down Expand Up @@ -134,10 +138,11 @@ async function getExtensionPoints(extension: ExtensionInstance, url: string, bui
return payload
}

return {
const payloadWithAssets = {
...payload,
...(await mapManifestAssetsToPayload(manifestEntry, extensionPoint, url, extension)),
}
return payloadWithAssets
}),
)
}
Expand Down Expand Up @@ -206,6 +211,7 @@ async function intentsAssetMapper({
const intents = await Promise.all(
extensionPoint.intents.map(async (intent) => ({
...intent,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
schema: await getAssetPayload('schema', intent.schema as string, url, extension),
})),
)
Expand All @@ -215,12 +221,29 @@ async function intentsAssetMapper({

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

/**
* Mapper for compiled built assets (main, should_render).
* Reads the filepath directly from manifest.json so the bundleFolder prefix is preserved.
*/
async function builtAssetMapper({
identifier,
manifestValue,
url,
extension,
}: AssetMapperContext): Promise<Partial<DevNewExtensionPointSchema>> {
if (typeof manifestValue !== 'string') return {}
const payload = await getAssetPayload(identifier, manifestValue, url, extension)
return {assets: {[payload.name]: payload}}
}

/**
* Asset mappers registry - defines how each asset type should be handled.
* Assets not in this registry use the defaultAssetMapper.
*/
const ASSET_MAPPERS: {[key: string]: AssetMapper | undefined} = {
intents: intentsAssetMapper,
main: builtAssetMapper,
should_render: builtAssetMapper,
}

/**
Expand All @@ -236,7 +259,13 @@ async function mapManifestAssetsToPayload(
): Promise<Partial<DevNewExtensionPointSchema>> {
const mappingResults = await Promise.all(
Object.keys(manifestEntry).map(async (identifier) => {
const context: AssetMapperContext = {identifier, extensionPoint, url, extension}
const context: AssetMapperContext = {
identifier,
extensionPoint,
url,
extension,
manifestValue: manifestEntry[identifier],
}
return ASSET_MAPPERS[identifier]?.(context) ?? defaultAssetMapper(context)
}),
)
Expand Down
Loading