Skip to content

Commit 359e45f

Browse files
committed
Enable uploading tools schema
1 parent f7eb632 commit 359e45f

File tree

7 files changed

+102
-5
lines changed

7 files changed

+102
-5
lines changed

packages/app/src/cli/models/extensions/extension-instance.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
placeholderAppConfiguration,
1515
} from '../app/app.test-data.js'
1616
import {FunctionConfigType} from '../extensions/specifications/function.js'
17-
import {ExtensionBuildOptions} from '../../services/build/extension.js'
17+
import {ExtensionBuildOptions, buildUIExtension} from '../../services/build/extension.js'
1818
import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js'
1919
import {joinPath} from '@shopify/cli-kit/node/path'
2020
import {describe, expect, test, vi} from 'vitest'
@@ -27,6 +27,15 @@ import {Writable} from 'stream'
2727
const developerPlatformClient: DeveloperPlatformClient = testDeveloperPlatformClient()
2828

2929
vi.mock('@shopify/cli-kit/node/import-extractor')
30+
vi.mock('../../services/build/extension.js', async () => {
31+
const actual = await vi.importActual('../../services/build/extension.js')
32+
return {
33+
...actual,
34+
buildUIExtension: vi.fn(),
35+
buildThemeExtension: vi.fn(),
36+
buildFunctionExtension: vi.fn(),
37+
}
38+
})
3039

3140
function functionConfiguration(): FunctionConfigType {
3241
return {
@@ -156,6 +165,33 @@ describe('build', async () => {
156165
})
157166
})
158167

168+
test('calls copyStaticAssets after buildUIExtension when building UI extensions', async () => {
169+
await inTemporaryDirectory(async (tmpDir) => {
170+
// Given
171+
const extensionInstance = await testUIExtension({
172+
type: 'ui_extension',
173+
directory: tmpDir,
174+
})
175+
176+
const copyStaticAssetsSpy = vi.spyOn(extensionInstance, 'copyStaticAssets').mockResolvedValue()
177+
vi.mocked(buildUIExtension).mockResolvedValue()
178+
179+
const options: ExtensionBuildOptions = {
180+
stdout: new Writable({write: vi.fn()}),
181+
stderr: new Writable({write: vi.fn()}),
182+
app: testApp(),
183+
environment: 'production',
184+
}
185+
186+
// When
187+
await extensionInstance.build(options)
188+
189+
// Then
190+
expect(buildUIExtension).toHaveBeenCalledWith(extensionInstance, options)
191+
expect(copyStaticAssetsSpy).toHaveBeenCalledOnce()
192+
})
193+
})
194+
159195
test('does not copy shopify.extension.toml file when bundling theme extensions', async () => {
160196
await inTemporaryDirectory(async (tmpDir) => {
161197
// Given

packages/app/src/cli/models/extensions/extension-instance.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,9 @@ export class ExtensionInstance<TConfiguration extends BaseConfigType = BaseConfi
352352
case 'function':
353353
return buildFunctionExtension(this, options)
354354
case 'ui':
355-
return buildUIExtension(this, options)
355+
await buildUIExtension(this, options)
356+
// Copy static assets after build completes
357+
return this.copyStaticAssets()
356358
case 'tax_calculation':
357359
await touchFile(this.outputPath)
358360
await writeFile(this.outputPath, '(()=>{})();')
@@ -492,6 +494,16 @@ export class ExtensionInstance<TConfiguration extends BaseConfigType = BaseConfi
492494
return [...new Set(watchedFiles.map((file) => normalizePath(file)))]
493495
}
494496

497+
/**
498+
* Copy static assets from the extension directory to the output path
499+
* Used by both dev and deploy builds
500+
*/
501+
async copyStaticAssets(outputPath?: string) {
502+
if (this.specification.copyStaticAssets) {
503+
return this.specification.copyStaticAssets(this.configuration, this.directory, outputPath ?? this.outputPath)
504+
}
505+
}
506+
495507
/**
496508
* Rescans imports for this extension and updates the cached import paths
497509
* Returns true if the imports changed

packages/app/src/cli/models/extensions/schemas.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const NewExtensionPointSchema = zod.object({
4545
target: zod.string(),
4646
module: zod.string(),
4747
should_render: ShouldRenderSchema.optional(),
48+
tools: zod.string().optional(),
4849
metafields: zod.array(MetafieldSchema).optional(),
4950
default_placement: zod.string().optional(),
5051
urls: zod

packages/app/src/cli/models/extensions/specification.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type UidStrategy = 'single' | 'dynamic' | 'uuid'
3838
export enum AssetIdentifier {
3939
ShouldRender = 'should_render',
4040
Main = 'main',
41+
Tools = 'tools',
4142
}
4243

4344
export interface Asset {
@@ -118,6 +119,11 @@ export interface ExtensionSpecification<TConfiguration extends BaseConfigType =
118119
extension: ExtensionInstance<TConfiguration>,
119120
typeDefinitionsByFile: Map<string, Set<string>>,
120121
) => Promise<void>
122+
123+
/**
124+
* Copy static assets from the extension directory to the output path
125+
*/
126+
copyStaticAssets?: (configuration: TConfiguration, directory: string, outputPath: string) => Promise<void>
121127
}
122128

123129
/**

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

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import {loadLocalesConfig} from '../../../utilities/extensions/locales-configura
55
import {getExtensionPointTargetSurface} from '../../../services/dev/extension/utilities.js'
66
import {ExtensionInstance} from '../extension-instance.js'
77
import {err, ok, Result} from '@shopify/cli-kit/node/result'
8-
import {fileExists} from '@shopify/cli-kit/node/fs'
9-
import {joinPath} from '@shopify/cli-kit/node/path'
8+
import {copyFile, fileExists} from '@shopify/cli-kit/node/fs'
9+
import {joinPath, basename, dirname} from '@shopify/cli-kit/node/path'
1010
import {outputContent, outputToken} from '@shopify/cli-kit/node/output'
1111
import {zod} from '@shopify/cli-kit/node/schema'
1212

@@ -27,6 +27,7 @@ export interface BuildManifest {
2727
[key in AssetIdentifier]?: {
2828
filepath: string
2929
module?: string
30+
static?: boolean
3031
}
3132
}
3233
}
@@ -57,6 +58,15 @@ export const UIExtensionSchema = BaseSchema.extend({
5758
},
5859
}
5960
: null),
61+
...(targeting.tools
62+
? {
63+
[AssetIdentifier.Tools]: {
64+
filepath: `${config.handle}-${AssetIdentifier.Tools}-${basename(targeting.tools)}`,
65+
module: targeting.tools,
66+
static: true,
67+
},
68+
}
69+
: null),
6070
},
6171
}
6272

@@ -125,6 +135,11 @@ const uiExtensionSpec = createExtensionSpecification({
125135
return
126136
}
127137

138+
// Skip static assets - they are copied after esbuild completes in rebuildContext
139+
if (asset.static && asset.module) {
140+
return
141+
}
142+
128143
assets[identifier] = {
129144
identifier: identifier as AssetIdentifier,
130145
outputFileName: asset.filepath,
@@ -143,6 +158,26 @@ const uiExtensionSpec = createExtensionSpecification({
143158
...(assetsArray.length ? {assets: assetsArray} : {}),
144159
}
145160
},
161+
copyStaticAssets: async (config, directory, outputPath) => {
162+
if (!isRemoteDomExtension(config)) return
163+
164+
await Promise.all(
165+
config.extension_points.map((extensionPoint) => {
166+
if (!('build_manifest' in extensionPoint)) return Promise.resolve()
167+
168+
return Object.entries(extensionPoint.build_manifest.assets).map(([_, asset]) => {
169+
if (asset.static && asset.module) {
170+
const sourceFile = joinPath(directory, asset.module)
171+
const outputFilePath = joinPath(dirname(outputPath), asset.filepath)
172+
return copyFile(sourceFile, outputFilePath).catch((error) => {
173+
throw new Error(`Failed to copy static asset ${asset.module} to ${outputFilePath}: ${error.message}`)
174+
})
175+
}
176+
return Promise.resolve()
177+
})
178+
}),
179+
)
180+
},
146181
hasExtensionPointTarget: (config, requestedTarget) => {
147182
return (
148183
config.extension_points?.find((extensionPoint) => {

packages/app/src/cli/services/dev/app-events/app-watcher-esbuild.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,13 +154,15 @@ describe('app-watcher-esbuild', () => {
154154
const manager = new ESBuildContextManager(options)
155155
await manager.createContexts([extension1])
156156
const spyContext = vi.spyOn(manager.contexts.uid1![0]!, 'rebuild').mockResolvedValue({} as any)
157+
const spyCopyStaticAssets = vi.spyOn(extension1, 'copyStaticAssets').mockResolvedValue()
157158
const spyCopy = vi.spyOn(fs, 'copyFile').mockResolvedValue()
158159

159160
// When
160161
await manager.rebuildContext(extension1)
161162

162163
// Then
163164
expect(spyContext).toHaveBeenCalled()
165+
expect(spyCopyStaticAssets).toHaveBeenCalledWith('/path/to/output/uid1/dist/test-ui-extension.js')
164166
expect(spyCopy).toHaveBeenCalledWith('/path/to/output/uid1/dist', '/extensions/ui_extension_1/dist')
165167
})
166168

packages/app/src/cli/services/dev/app-events/app-watcher-esbuild.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,19 @@ export class ESBuildContextManager {
7979
const context = this.contexts[extension.uid]
8080
if (!context) return
8181
await Promise.all(context.map((ctxt) => ctxt.rebuild()))
82+
const devBundleOutputPath = extension.getOutputPathForDirectory(this.outputPath)
83+
84+
// Copy static assets after build completes
85+
// Pass in an explicit output path because the extension.outputPath is not the same as the dev bundle output path.
86+
await extension.copyStaticAssets(devBundleOutputPath)
8287

8388
// The default output path for a extension is now inside `.shopify/bundle/<ext_id>/dist`,
8489
// all extensions output need to be under the same directory so that it can all be zipped together.
8590

8691
// But historically the output was inside each extension's directory.
8792
// To avoid breaking flows that depend on this, we copy the output to the old location.
8893
// This also makes it easier to access sourcemaps or other built artifacts.
89-
const outputPath = dirname(extension.getOutputPathForDirectory(this.outputPath))
94+
const outputPath = dirname(devBundleOutputPath)
9095
const copyPath = dirname(extension.outputPath)
9196
await copyFile(outputPath, copyPath)
9297
}

0 commit comments

Comments
 (0)