Skip to content

Commit 8342f70

Browse files
elanalynnalfonso-noriega
authored andcommitted
Asset pipeline flow for hosted static app
1 parent 0202391 commit 8342f70

File tree

6 files changed

+148
-2
lines changed

6 files changed

+148
-2
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {PrivacyComplianceWebhooksSpecIdentifier} from './specifications/app_conf
1111
import {WebhooksSpecIdentifier} from './specifications/app_config_webhook.js'
1212
import {WebhookSubscriptionSpecIdentifier} from './specifications/app_config_webhook_subscription.js'
1313
import {EventsSpecIdentifier} from './specifications/app_config_events.js'
14+
import {HostedAppHomeSpecIdentifier} from './specifications/app_config_hosted_app_home.js'
1415
import {
1516
ExtensionBuildOptions,
1617
buildFunctionExtension,
@@ -39,6 +40,7 @@ export const CONFIG_EXTENSION_IDS: string[] = [
3940
AppHomeSpecIdentifier,
4041
AppProxySpecIdentifier,
4142
BrandingSpecIdentifier,
43+
HostedAppHomeSpecIdentifier,
4244
PosSpecIdentifier,
4345
PrivacyComplianceWebhooksSpecIdentifier,
4446
WebhookSubscriptionSpecIdentifier,
@@ -371,6 +373,9 @@ export class ExtensionInstance<TConfiguration extends BaseConfigType = BaseConfi
371373
this.specification.buildConfig.filePatterns,
372374
this.specification.buildConfig.ignoredFilePatterns,
373375
)
376+
case 'static_app':
377+
await this.copyStaticAssets()
378+
break
374379
case 'none':
375380
break
376381
}

packages/app/src/cli/models/extensions/load-specifications.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import uiExtensionSpec from './specifications/ui_extension.js'
2727
import webPixelSpec from './specifications/web_pixel_extension.js'
2828
import editorExtensionCollectionSpecification from './specifications/editor_extension_collection.js'
2929
import channelSpecificationSpec from './specifications/channel.js'
30+
import hostedAppHomeSpec, {HostedAppHomeSpecIdentifier} from './specifications/app_config_hosted_app_home.js'
3031

3132
const SORTED_CONFIGURATION_SPEC_IDENTIFIERS = [
3233
BrandingSpecIdentifier,
@@ -38,6 +39,7 @@ const SORTED_CONFIGURATION_SPEC_IDENTIFIERS = [
3839
AppProxySpecIdentifier,
3940
PosSpecIdentifier,
4041
AppHomeSpecIdentifier,
42+
HostedAppHomeSpecIdentifier,
4143
]
4244

4345
/**
@@ -61,6 +63,7 @@ function loadSpecifications() {
6163
appWebhooksSpec,
6264
appWebhookSubscriptionSpec,
6365
appEventsSpec,
66+
hostedAppHomeSpec,
6467
]
6568
const moduleSpecs = [
6669
checkoutPostPurchaseSpec,

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export interface BuildAsset {
5454
}
5555

5656
type BuildConfig =
57-
| {mode: 'ui' | 'theme' | 'function' | 'tax_calculation' | 'none'}
57+
| {mode: 'ui' | 'theme' | 'function' | 'tax_calculation' | 'none' | 'static_app'}
5858
| {mode: 'copy_files'; filePatterns: string[]; ignoredFilePatterns?: string[]}
5959
/**
6060
* Extension specification with all the needed properties and methods to load an extension.
@@ -245,11 +245,13 @@ export function createExtensionSpecification<TConfiguration extends BaseConfigTy
245245
export function createConfigExtensionSpecification<TConfiguration extends BaseConfigType = BaseConfigType>(spec: {
246246
identifier: string
247247
schema: ZodSchemaType<TConfiguration>
248+
buildConfig?: BuildConfig
248249
appModuleFeatures?: (config?: TConfiguration) => ExtensionFeature[]
249250
transformConfig: TransformationConfig | CustomTransformationConfig
250251
uidStrategy?: UidStrategy
251252
getDevSessionUpdateMessages?: (config: TConfiguration) => Promise<string[]>
252253
patchWithAppDevURLs?: (config: TConfiguration, urls: ApplicationURLs) => void
254+
copyStaticAssets?: (config: TConfiguration, directory: string, outputPath: string) => Promise<void>
253255
}): ExtensionSpecification<TConfiguration> {
254256
const appModuleFeatures = spec.appModuleFeatures ?? (() => [])
255257
return createExtensionSpecification({
@@ -262,8 +264,10 @@ export function createConfigExtensionSpecification<TConfiguration extends BaseCo
262264
transformRemoteToLocal: resolveReverseAppConfigTransform(spec.schema, spec.transformConfig),
263265
experience: 'configuration',
264266
uidStrategy: spec.uidStrategy ?? 'single',
267+
buildConfig: spec.buildConfig ?? {mode: 'none'},
265268
getDevSessionUpdateMessages: spec.getDevSessionUpdateMessages,
266269
patchWithAppDevURLs: spec.patchWithAppDevURLs,
270+
copyStaticAssets: spec.copyStaticAssets,
267271
})
268272
}
269273

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import spec from './app_config_hosted_app_home.js'
2+
import {placeholderAppConfiguration} from '../../app/app.test-data.js'
3+
import {copyDirectoryContents} from '@shopify/cli-kit/node/fs'
4+
import {describe, expect, test, vi} from 'vitest'
5+
6+
vi.mock('@shopify/cli-kit/node/fs')
7+
8+
describe('hosted_app_home', () => {
9+
describe('transform', () => {
10+
test('should return the transformed object with static_root', () => {
11+
const object = {
12+
static_root: 'public',
13+
}
14+
const appConfigSpec = spec
15+
16+
const result = appConfigSpec.transformLocalToRemote!(object, placeholderAppConfiguration)
17+
18+
expect(result).toMatchObject({
19+
static_root: 'public',
20+
})
21+
})
22+
23+
test('should return empty object when static_root is not provided', () => {
24+
const object = {}
25+
const appConfigSpec = spec
26+
27+
const result = appConfigSpec.transformLocalToRemote!(object, placeholderAppConfiguration)
28+
29+
expect(result).toMatchObject({})
30+
})
31+
})
32+
33+
describe('reverseTransform', () => {
34+
test('should return the reversed transformed object with static_root', () => {
35+
const object = {
36+
static_root: 'public',
37+
}
38+
const appConfigSpec = spec
39+
40+
const result = appConfigSpec.transformRemoteToLocal!(object)
41+
42+
expect(result).toMatchObject({
43+
static_root: 'public',
44+
})
45+
})
46+
47+
test('should return empty object when static_root is not provided', () => {
48+
const object = {}
49+
const appConfigSpec = spec
50+
51+
const result = appConfigSpec.transformRemoteToLocal!(object)
52+
53+
expect(result).toMatchObject({})
54+
})
55+
})
56+
57+
describe('copyStaticAssets', () => {
58+
test('should copy static assets from source to output directory', async () => {
59+
vi.mocked(copyDirectoryContents).mockResolvedValue(undefined)
60+
const config = {static_root: 'public'}
61+
const directory = '/app/root'
62+
const outputPath = '/output/dist/bundle.js'
63+
64+
await spec.copyStaticAssets!(config, directory, outputPath)
65+
66+
expect(copyDirectoryContents).toHaveBeenCalledWith('/app/root/public', '/output/dist')
67+
})
68+
69+
test('should not copy assets when static_root is not provided', async () => {
70+
const config = {}
71+
const directory = '/app/root'
72+
const outputPath = '/output/dist/bundle.js'
73+
74+
await spec.copyStaticAssets!(config, directory, outputPath)
75+
76+
expect(copyDirectoryContents).not.toHaveBeenCalled()
77+
})
78+
79+
test('should throw error when copy fails', async () => {
80+
vi.mocked(copyDirectoryContents).mockRejectedValue(new Error('Permission denied'))
81+
const config = {static_root: 'public'}
82+
const directory = '/app/root'
83+
const outputPath = '/output/dist/bundle.js'
84+
85+
await expect(spec.copyStaticAssets!(config, directory, outputPath)).rejects.toThrow(
86+
'Failed to copy static assets from /app/root/public to /output/dist: Permission denied',
87+
)
88+
})
89+
})
90+
91+
describe('buildConfig', () => {
92+
test('should have static_app build mode', () => {
93+
expect(spec.buildConfig).toEqual({mode: 'static_app'})
94+
})
95+
})
96+
97+
describe('identifier', () => {
98+
test('should have correct identifier', () => {
99+
expect(spec.identifier).toBe('hosted_app')
100+
})
101+
})
102+
})
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {BaseSchemaWithoutHandle} from '../schemas.js'
2+
import {TransformationConfig, createConfigExtensionSpecification} from '../specification.js'
3+
import {copyDirectoryContents} from '@shopify/cli-kit/node/fs'
4+
import {dirname, joinPath} from '@shopify/cli-kit/node/path'
5+
import {zod} from '@shopify/cli-kit/node/schema'
6+
7+
const HostedAppHomeSchema = BaseSchemaWithoutHandle.extend({
8+
static_root: zod.string().optional(),
9+
})
10+
11+
const HostedAppHomeTransformConfig: TransformationConfig = {
12+
static_root: 'static_root',
13+
}
14+
15+
export const HostedAppHomeSpecIdentifier = 'hosted_app'
16+
17+
const hostedAppHomeSpec = createConfigExtensionSpecification({
18+
identifier: HostedAppHomeSpecIdentifier,
19+
buildConfig: {mode: 'static_app'} as const,
20+
schema: HostedAppHomeSchema,
21+
transformConfig: HostedAppHomeTransformConfig,
22+
copyStaticAssets: async (config, directory, outputPath) => {
23+
if (!config.static_root) return
24+
const sourceDir = joinPath(directory, config.static_root)
25+
const outputDir = dirname(outputPath)
26+
27+
return copyDirectoryContents(sourceDir, outputDir).catch((error) => {
28+
throw new Error(`Failed to copy static assets from ${sourceDir} to ${outputDir}: ${error.message}`)
29+
})
30+
},
31+
})
32+
33+
export default hostedAppHomeSpec

packages/app/src/cli/services/generate/fetch-extension-specifications.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ async function mergeLocalAndRemoteSpecs(
7878

7979
const merged = {...localSpec, ...remoteSpec, loadedRemoteSpecs: true} as RemoteAwareExtensionSpecification &
8080
FlattenedRemoteSpecification
81-
8281
// If configuration is inside an app.toml -- i.e. single UID mode -- we must be able to parse a partial slice.
8382
let handleInvalidAdditionalProperties: HandleInvalidAdditionalProperties
8483
switch (merged.uidStrategy) {

0 commit comments

Comments
 (0)