-
Notifications
You must be signed in to change notification settings - Fork 228
Refactor extension build process to use modular build steps #6867
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 01-build-steps-infrastructure
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,7 +16,7 @@ export const HostedAppHomeSpecIdentifier = 'hosted_app_home' | |
|
|
||
| const hostedAppHomeSpec = createConfigExtensionSpecification({ | ||
| identifier: HostedAppHomeSpecIdentifier, | ||
| buildConfig: {mode: 'hosted_app_home'} as const, | ||
| buildConfig: {mode: 'none'} as const, | ||
| schema: HostedAppHomeSchema, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hosted app home build is effectively disabled (behavior change) The PR changes Evidence (new behavior):
Impact: Hosted app home extensions can ship without static assets being copied into the build output. End users (merchants) could see missing UI/resources; deployments may succeed but produce broken runtime behavior. This affects all users of that extension type. |
||
| transformConfig: HostedAppHomeTransformConfig, | ||
| copyStaticAssets: async (config, directory, outputPath) => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| import spec from './channel.js' | ||
| import {ExtensionInstance} from '../extension-instance.js' | ||
| import {ExtensionBuildOptions} from '../../../services/build/extension.js' | ||
| import {describe, expect, test} from 'vitest' | ||
| import {inTemporaryDirectory, writeFile, fileExists, mkdir} from '@shopify/cli-kit/node/fs' | ||
| import {joinPath} from '@shopify/cli-kit/node/path' | ||
| import {Writable} from 'stream' | ||
|
|
||
| const SUBDIRECTORY = 'specifications' | ||
|
|
||
| describe('channel_config', () => { | ||
| describe('clientSteps', () => { | ||
| test('uses copy_files mode', () => { | ||
| expect(spec.buildConfig.mode).toBe('copy_files') | ||
| }) | ||
|
|
||
| test('has a single copy-files step scoped to the specifications subdirectory', () => { | ||
| expect(spec.clientSteps[0]!.steps).toHaveLength(1) | ||
| expect(spec.clientSteps[0]!.steps[0]).toMatchObject({ | ||
| id: 'copy-files', | ||
| type: 'include_assets', | ||
| config: { | ||
| inclusions: [{type: 'pattern', baseDir: SUBDIRECTORY, destination: SUBDIRECTORY}], | ||
| }, | ||
| }) | ||
|
|
||
| const {include} = ( | ||
| spec.clientSteps[0]!.steps[0]!.config as {inclusions: [{include: string[]}]} | ||
| ).inclusions[0]! | ||
|
|
||
| expect(include).toEqual(expect.arrayContaining(['**/*.json', '**/*.toml', '**/*.yaml', '**/*.yml', '**/*.svg'])) | ||
| }) | ||
|
|
||
| test('config is serializable to JSON', () => { | ||
| const serialized = JSON.stringify(spec.clientSteps) | ||
| const deserialized = JSON.parse(serialized) | ||
|
|
||
| expect(deserialized[0].steps).toHaveLength(1) | ||
| expect(deserialized[0].steps[0].config.inclusions[0].type).toBe('pattern') | ||
| }) | ||
| }) | ||
|
|
||
| describe('build integration', () => { | ||
| test('copies specification files to output, preserving subdirectory structure', async () => { | ||
| await inTemporaryDirectory(async (tmpDir) => { | ||
| // Given | ||
| const extensionDir = joinPath(tmpDir, 'extension') | ||
| const specsDir = joinPath(extensionDir, SUBDIRECTORY) | ||
| const outputDir = joinPath(tmpDir, 'output') | ||
|
|
||
| await mkdir(specsDir) | ||
| await mkdir(outputDir) | ||
|
|
||
| await writeFile(joinPath(specsDir, 'product.json'), '{}') | ||
| await writeFile(joinPath(specsDir, 'order.toml'), '[spec]') | ||
| await writeFile(joinPath(specsDir, 'logo.svg'), '<svg/>') | ||
| // Root-level files should NOT be copied | ||
| await writeFile(joinPath(extensionDir, 'README.md'), '# readme') | ||
| await writeFile(joinPath(extensionDir, 'index.js'), 'ignored') | ||
|
|
||
| const extension = new ExtensionInstance({ | ||
| configuration: {name: 'my-channel', type: 'channel'}, | ||
| configurationPath: '', | ||
| directory: extensionDir, | ||
| specification: spec, | ||
| }) | ||
| extension.outputPath = outputDir | ||
|
|
||
| const buildOptions: ExtensionBuildOptions = { | ||
| stdout: new Writable({ | ||
| write(chunk, enc, cb) { | ||
| cb() | ||
| }, | ||
| }), | ||
| stderr: new Writable({ | ||
| write(chunk, enc, cb) { | ||
| cb() | ||
| }, | ||
| }), | ||
| app: {} as any, | ||
| environment: 'production', | ||
| } | ||
|
|
||
| // When | ||
| await extension.build(buildOptions) | ||
|
|
||
| // Then — specification files copied with path preserved | ||
| await expect(fileExists(joinPath(outputDir, SUBDIRECTORY, 'product.json'))).resolves.toBe(true) | ||
| await expect(fileExists(joinPath(outputDir, SUBDIRECTORY, 'order.toml'))).resolves.toBe(true) | ||
| await expect(fileExists(joinPath(outputDir, SUBDIRECTORY, 'logo.svg'))).resolves.toBe(true) | ||
|
|
||
| // Root-level files not in specifications/ are not copied | ||
| await expect(fileExists(joinPath(outputDir, 'README.md'))).resolves.toBe(false) | ||
| await expect(fileExists(joinPath(outputDir, 'index.js'))).resolves.toBe(false) | ||
| }) | ||
| }) | ||
|
|
||
| test('does not copy files with non-matching extensions inside specifications/', async () => { | ||
| await inTemporaryDirectory(async (tmpDir) => { | ||
| // Given | ||
| const extensionDir = joinPath(tmpDir, 'extension') | ||
| const specsDir = joinPath(extensionDir, SUBDIRECTORY) | ||
| const outputDir = joinPath(tmpDir, 'output') | ||
|
|
||
| await mkdir(specsDir) | ||
| await mkdir(outputDir) | ||
|
|
||
| await writeFile(joinPath(specsDir, 'spec.json'), '{}') | ||
| await writeFile(joinPath(specsDir, 'ignored.ts'), 'const x = 1') | ||
| await writeFile(joinPath(specsDir, 'ignored.js'), 'const x = 1') | ||
|
|
||
| const extension = new ExtensionInstance({ | ||
| configuration: {name: 'my-channel', type: 'channel'}, | ||
| configurationPath: '', | ||
| directory: extensionDir, | ||
| specification: spec, | ||
| }) | ||
| extension.outputPath = outputDir | ||
|
|
||
| const buildOptions: ExtensionBuildOptions = { | ||
| stdout: new Writable({ | ||
| write(chunk, enc, cb) { | ||
| cb() | ||
| }, | ||
| }), | ||
| stderr: new Writable({ | ||
| write(chunk, enc, cb) { | ||
| cb() | ||
| }, | ||
| }), | ||
| app: {} as any, | ||
| environment: 'production', | ||
| } | ||
|
|
||
| // When | ||
| await extension.build(buildOptions) | ||
|
|
||
| // Then | ||
| await expect(fileExists(joinPath(outputDir, SUBDIRECTORY, 'spec.json'))).resolves.toBe(true) | ||
| await expect(fileExists(joinPath(outputDir, SUBDIRECTORY, 'ignored.ts'))).resolves.toBe(false) | ||
| await expect(fileExists(joinPath(outputDir, SUBDIRECTORY, 'ignored.js'))).resolves.toBe(false) | ||
| }) | ||
| }) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Duplicate/double-wrapped error handling in ExtensionInstance.build()
executeStep()already throws when a step fails andcontinueOnErroris false:But
ExtensionInstance.build()then checks:That condition is effectively unreachable because
executeStep()won’t return{success:false}unlesscontinueOnErroris true. This is dead logic and also risks producing a worse error message (e.g.,undefined) if behavior changes later.Impact: