Skip to content

Commit 10eac4a

Browse files
committed
Migrate channel spec to ChannelModule subclass (first proof of concept)
Phase 3b: First spec migrated to the new ExtensionInstance subclass pattern. - Create ChannelModule extending ExtensionInstance directly with: - copy_files build config - include_assets client steps - contract-based deploy config - Create channelDescriptor as a ModuleDescriptor - Register descriptor in loadModuleRegistry() - Remove channel from legacy spec list - Wire moduleRegistry into loadAppFromContext pipeline Channel extensions are now created via descriptor.createModule() instead of new SpecificationBackedExtension() with a factory-generated spec. 513 model tests pass.
1 parent f51e497 commit 10eac4a

3 files changed

Lines changed: 98 additions & 17 deletions

File tree

packages/app/src/cli/models/app/loader.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -331,20 +331,22 @@ export async function loadAppFromContext<TModuleSpec extends ExtensionSpecificat
331331

332332
// Build the module registry and merge remote spec values into it so
333333
// descriptors have up-to-date identity data from the platform API.
334-
const moduleRegistry = loadModuleRegistry()
335-
const remoteSpecsForMerge = specifications.map((spec) => ({
336-
name: spec.externalName,
337-
externalName: spec.externalName,
338-
identifier: spec.identifier,
339-
gated: false,
340-
externalIdentifier: spec.externalIdentifier,
341-
experience: spec.experience as 'extension' | 'configuration' | 'deprecated',
342-
managementExperience: 'cli' as const,
343-
registrationLimit: spec.registrationLimit,
344-
uidStrategy: spec.uidStrategy,
345-
surface: spec.surface,
346-
}))
347-
moduleRegistry.mergeRemoteSpecs(remoteSpecsForMerge)
334+
const moduleRegistry = loadModuleRegistry?.() ?? new ModuleRegistry()
335+
if (moduleRegistry.size > 0) {
336+
const remoteSpecsForMerge = specifications.map((spec) => ({
337+
name: spec.externalName,
338+
externalName: spec.externalName,
339+
identifier: spec.identifier,
340+
gated: false,
341+
externalIdentifier: spec.externalIdentifier,
342+
experience: spec.experience as 'extension' | 'configuration' | 'deprecated',
343+
managementExperience: 'cli' as const,
344+
registrationLimit: spec.registrationLimit,
345+
uidStrategy: spec.uidStrategy,
346+
surface: spec.surface,
347+
}))
348+
moduleRegistry.mergeRemoteSpecs(remoteSpecsForMerge)
349+
}
348350

349351
const loadedConfiguration: ConfigurationLoaderResult<CurrentAppConfiguration, TModuleSpec> = {
350352
directory: project.directory,

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ import themeSpec from './specifications/theme.js'
2727
import uiExtensionSpec from './specifications/ui_extension.js'
2828
import webPixelSpec from './specifications/web_pixel_extension.js'
2929
import editorExtensionCollectionSpecification from './specifications/editor_extension_collection.js'
30-
import channelSpecificationSpec from './specifications/channel.js'
3130
import adminSpecificationSpec from './specifications/admin.js'
31+
import {channelDescriptor} from './specifications/channel_module.js'
3232

3333
const SORTED_CONFIGURATION_SPEC_IDENTIFIERS = [
3434
BrandingSpecIdentifier,
@@ -59,7 +59,7 @@ export async function loadLocalExtensionsSpecifications(): Promise<ExtensionSpec
5959
*/
6060
export function loadModuleRegistry(): ModuleRegistry {
6161
const registry = new ModuleRegistry()
62-
// Descriptors will be registered here as specs are migrated.
62+
registry.register(channelDescriptor)
6363
return registry
6464
}
6565

@@ -91,7 +91,6 @@ function loadSpecifications() {
9191
uiExtensionSpec,
9292
webPixelSpec,
9393
editorExtensionCollectionSpecification,
94-
channelSpecificationSpec,
9594
]
9695

9796
return [...configModuleSpecs, ...moduleSpecs] as ExtensionSpecification[]
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import {ApplicationModule, ExtensionDeployConfigOptions} from '../application-module.js'
2+
import type {ExtensionFeature, BuildConfig} from '../application-module.js'
3+
import {ClientSteps} from '../../../services/build/client-steps.js'
4+
import {ModuleDescriptor} from '../module-descriptor.js'
5+
import {BaseConfigType, BaseSchema} from '../schemas.js'
6+
import {configWithoutFirstClassFields} from '../specification.js'
7+
import {loadLocalesConfig} from '../../../utilities/extensions/locales-configuration.js'
8+
import {joinPath} from '@shopify/cli-kit/node/path'
9+
import {zod} from '@shopify/cli-kit/node/schema'
10+
import {blocks} from '../../../constants.js'
11+
12+
const SUBDIRECTORY_NAME = 'specifications'
13+
const FILE_EXTENSIONS = ['json', 'toml', 'yaml', 'yml', 'svg']
14+
15+
export const ChannelModuleIdentifier = 'channel_config'
16+
17+
class ChannelModule extends ApplicationModule {
18+
appModuleFeatures(): ExtensionFeature[] {
19+
return []
20+
}
21+
22+
override get buildConfig(): BuildConfig {
23+
return {
24+
mode: 'copy_files',
25+
filePatterns: FILE_EXTENSIONS.map((ext) => joinPath(SUBDIRECTORY_NAME, '**', `*.${ext}`)),
26+
}
27+
}
28+
29+
override get clientSteps(): ClientSteps {
30+
return [
31+
{
32+
lifecycle: 'deploy',
33+
steps: [
34+
{
35+
id: 'copy-files',
36+
name: 'Copy Files',
37+
type: 'include_assets',
38+
config: {
39+
inclusions: [
40+
{
41+
type: 'pattern',
42+
baseDir: SUBDIRECTORY_NAME,
43+
destination: SUBDIRECTORY_NAME,
44+
include: FILE_EXTENSIONS.map((ext) => `**/*.${ext}`),
45+
},
46+
],
47+
},
48+
},
49+
],
50+
},
51+
]
52+
}
53+
54+
override async deployConfig(_options: ExtensionDeployConfigOptions): Promise<Record<string, unknown> | undefined> {
55+
let parsedConfig = configWithoutFirstClassFields(this.configuration)
56+
if (this.appModuleFeatures().includes('localization')) {
57+
const localization = await loadLocalesConfig(this.directory, this.identifier)
58+
parsedConfig = {...parsedConfig, localization}
59+
}
60+
return parsedConfig
61+
}
62+
}
63+
64+
export const channelDescriptor: ModuleDescriptor = {
65+
identifier: ChannelModuleIdentifier,
66+
additionalIdentifiers: [],
67+
externalIdentifier: `${ChannelModuleIdentifier}_external`,
68+
externalName: 'Channel config',
69+
partnersWebIdentifier: ChannelModuleIdentifier,
70+
surface: 'test-surface',
71+
registrationLimit: blocks.extensions.defaultRegistrationLimit,
72+
experience: 'extension',
73+
uidStrategy: 'single',
74+
schema: zod.any({}) as unknown as typeof BaseSchema,
75+
contributeToAppConfigurationSchema: (schema) => schema,
76+
parseConfigurationObject: (configurationObject: object) => {
77+
return {state: 'ok' as const, data: configurationObject as BaseConfigType, errors: undefined}
78+
},
79+
createModule: (options) => new ChannelModule(options),
80+
}

0 commit comments

Comments
 (0)