Skip to content

Commit 1d8faaf

Browse files
committed
Add new copy_files build mode and apply it to flow
1 parent e9d37ac commit 1d8faaf

File tree

4 files changed

+155
-6
lines changed

4 files changed

+155
-6
lines changed

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
buildUIExtension,
2121
bundleFunctionExtension,
2222
} from '../../services/build/extension.js'
23-
import {bundleThemeExtension} from '../../services/extensions/bundle.js'
23+
import {bundleThemeExtension, copyFilesForExtension} from '../../services/extensions/bundle.js'
2424
import {Identifiers} from '../app/identifiers.js'
2525
import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js'
2626
import {AppConfigurationWithoutPath} from '../app/app.js'
@@ -355,6 +355,13 @@ export class ExtensionInstance<TConfiguration extends BaseConfigType = BaseConfi
355355
await touchFile(this.outputPath)
356356
await writeFile(this.outputPath, '(()=>{})();')
357357
break
358+
case 'copy_files':
359+
return copyFilesForExtension(
360+
this,
361+
options,
362+
this.specification.buildConfig.filePatterns,
363+
this.specification.buildConfig.ignoredFilePatterns,
364+
)
358365
case 'none':
359366
break
360367
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ export interface Asset {
4646
content: string
4747
}
4848

49-
interface BuildConfig {
50-
mode: 'ui' | 'theme' | 'flow' | 'function' | 'tax_calculation' | 'none'
51-
}
49+
type BuildConfig =
50+
| {mode: 'ui' | 'theme' | 'flow' | 'function' | 'tax_calculation' | 'none'}
51+
| {mode: 'copy_files'; filePatterns: string[]; ignoredFilePatterns?: string[]}
5252
/**
5353
* Extension specification with all the needed properties and methods to load an extension.
5454
*/

packages/app/src/cli/services/extensions/bundle.test.ts

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {bundleExtension, bundleThemeExtension} from './bundle.js'
1+
import {bundleExtension, bundleThemeExtension, copyFilesForExtension} from './bundle.js'
22
import {testApp, testUIExtension} from '../../models/app/app.test-data.js'
33
import {loadLocalExtensionsSpecifications} from '../../models/extensions/load-specifications.js'
44
import {ExtensionInstance} from '../../models/extensions/extension-instance.js'
@@ -277,3 +277,118 @@ describe('bundleExtension()', () => {
277277
})
278278
})
279279
})
280+
281+
describe('copyFilesForExtension()', () => {
282+
test('copies files matching include patterns to output directory', async () => {
283+
await inTemporaryDirectory(async (tmpDir) => {
284+
// Given
285+
const extensionDir = joinPath(tmpDir, 'extension')
286+
const outputDir = joinPath(tmpDir, 'output')
287+
288+
// Create extension directory structure
289+
await mkdir(joinPath(extensionDir, 'src'))
290+
await mkdir(joinPath(extensionDir, 'assets'))
291+
await mkdir(joinPath(extensionDir, 'assets', 'images'))
292+
293+
// Create test files
294+
touchFileSync(joinPath(extensionDir, 'config.json'))
295+
touchFileSync(joinPath(extensionDir, 'src', 'index.js'))
296+
touchFileSync(joinPath(extensionDir, 'src', 'styles.css'))
297+
touchFileSync(joinPath(extensionDir, 'assets', 'logo.png'))
298+
touchFileSync(joinPath(extensionDir, 'assets', 'images', 'banner.jpg'))
299+
300+
const extension = {
301+
directory: extensionDir,
302+
outputPath: outputDir,
303+
localIdentifier: 'test-extension',
304+
} as ExtensionInstance
305+
306+
const stdout = {write: vi.fn()}
307+
const stderr = {write: vi.fn()}
308+
const options = {stdout, stderr} as any
309+
310+
// When - copy all .json and .png files
311+
await copyFilesForExtension(extension, options, ['*.json', '*.png'], [])
312+
313+
// Then
314+
const copiedFiles = await glob(joinPath(outputDir, '**/*'))
315+
const relativeFiles = copiedFiles.map((file) => file.replace(`${outputDir}/`, ''))
316+
317+
expect(relativeFiles.sort()).toEqual(['assets/logo.png', 'config.json'])
318+
expect(stdout.write).toHaveBeenCalledWith('Copying files for extension test-extension...')
319+
expect(stdout.write).toHaveBeenCalledWith('test-extension successfully built')
320+
})
321+
})
322+
323+
test('respects ignore patterns when copying files', async () => {
324+
await inTemporaryDirectory(async (tmpDir) => {
325+
// Given
326+
const extensionDir = joinPath(tmpDir, 'extension')
327+
const outputDir = joinPath(tmpDir, 'output')
328+
329+
// Create extension directory structure
330+
await mkdir(joinPath(extensionDir, 'dist'))
331+
await mkdir(joinPath(extensionDir, 'src'))
332+
await mkdir(joinPath(extensionDir, 'test'))
333+
334+
// Create test files
335+
touchFileSync(joinPath(extensionDir, 'README.md'))
336+
touchFileSync(joinPath(extensionDir, 'package.json'))
337+
touchFileSync(joinPath(extensionDir, 'dist', 'bundle.js'))
338+
touchFileSync(joinPath(extensionDir, 'src', 'index.js'))
339+
touchFileSync(joinPath(extensionDir, 'test', 'test.js'))
340+
touchFileSync(joinPath(extensionDir, 'test', 'README.md'))
341+
342+
const extension = {
343+
directory: extensionDir,
344+
outputPath: outputDir,
345+
localIdentifier: 'test-extension',
346+
} as ExtensionInstance
347+
348+
const stdout = {write: vi.fn()}
349+
const stderr = {write: vi.fn()}
350+
const options = {stdout, stderr} as any
351+
352+
// When - copy all files but ignore test directory and dist files
353+
await copyFilesForExtension(extension, options, ['*'], ['test/**', 'dist/**'])
354+
355+
// Then
356+
const copiedFiles = await glob(joinPath(outputDir, '**/*'))
357+
const relativeFiles = copiedFiles.map((file) => file.replace(`${outputDir}/`, ''))
358+
359+
expect(relativeFiles.sort()).toEqual(['README.md', 'package.json', 'src/index.js'])
360+
// Verify ignored files were not copied
361+
expect(relativeFiles).not.toContain('test/test.js')
362+
expect(relativeFiles).not.toContain('test/README.md')
363+
expect(relativeFiles).not.toContain('dist/bundle.js')
364+
})
365+
})
366+
367+
test('handles empty include patterns gracefully', async () => {
368+
await inTemporaryDirectory(async (tmpDir) => {
369+
// Given
370+
const extensionDir = joinPath(tmpDir, 'extension')
371+
const outputDir = joinPath(tmpDir, 'output')
372+
373+
await mkdir(extensionDir)
374+
touchFileSync(joinPath(extensionDir, 'file.txt'))
375+
376+
const extension = {
377+
directory: extensionDir,
378+
outputPath: outputDir,
379+
localIdentifier: 'test-extension',
380+
} as ExtensionInstance
381+
382+
const stdout = {write: vi.fn()}
383+
const stderr = {write: vi.fn()}
384+
const options = {stdout, stderr} as any
385+
386+
// When - no include patterns provided
387+
await copyFilesForExtension(extension, options, [], [])
388+
389+
// Then - no files should be copied
390+
const copiedFiles = await glob(joinPath(outputDir, '**/*'))
391+
expect(copiedFiles).toEqual([])
392+
})
393+
})
394+
})

packages/app/src/cli/services/extensions/bundle.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {EsbuildEnvVarRegex, environmentVariableNames} from '../../constants.js'
55
import {flowTemplateExtensionFiles} from '../../utilities/extensions/flow-template.js'
66
import {context as esContext, formatMessagesSync} from 'esbuild'
77
import {AbortSignal} from '@shopify/cli-kit/node/abort'
8-
import {copyFile} from '@shopify/cli-kit/node/fs'
8+
import {copyFile, glob} from '@shopify/cli-kit/node/fs'
99
import {joinPath, relativePath} from '@shopify/cli-kit/node/path'
1010
import {outputDebug} from '@shopify/cli-kit/node/output'
1111
import {isTruthy} from '@shopify/cli-kit/node/context/utilities'
@@ -80,6 +80,32 @@ export async function bundleThemeExtension(
8080

8181
export async function bundleFlowTemplateExtension(extension: ExtensionInstance): Promise<void> {
8282
const files = await flowTemplateExtensionFiles(extension)
83+
84+
await Promise.all(
85+
files.map(function (filepath) {
86+
const relativePathName = relativePath(extension.directory, filepath)
87+
const outputFile = joinPath(extension.outputPath, relativePathName)
88+
if (filepath === outputFile) return
89+
return copyFile(filepath, outputFile)
90+
}),
91+
)
92+
}
93+
94+
export async function copyFilesForExtension(
95+
extension: ExtensionInstance,
96+
options: ExtensionBuildOptions,
97+
includePatterns: string[],
98+
ignoredPatterns: string[] = [],
99+
): Promise<void> {
100+
options.stdout.write(`Copying files for extension ${extension.localIdentifier}...`)
101+
const include = includePatterns.map((pattern) => joinPath('**', pattern))
102+
const ignored = ignoredPatterns.map((pattern) => joinPath('**', pattern))
103+
const files = await glob(include, {
104+
absolute: true,
105+
cwd: extension.directory,
106+
ignore: ignored,
107+
})
108+
83109
await Promise.all(
84110
files.map(function (filepath) {
85111
const relativePathName = relativePath(extension.directory, filepath)
@@ -88,6 +114,7 @@ export async function bundleFlowTemplateExtension(extension: ExtensionInstance):
88114
return copyFile(filepath, outputFile)
89115
}),
90116
)
117+
options.stdout.write(`${extension.localIdentifier} successfully built`)
91118
}
92119

93120
function onResult(result: Awaited<ReturnType<typeof esBuild>> | null, options: BundleOptions) {

0 commit comments

Comments
 (0)