Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import {copyByPattern} from './copy-by-pattern.js'
import {describe, expect, test, vi, beforeEach} from 'vitest'
import * as fs from '@shopify/cli-kit/node/fs'

vi.mock('@shopify/cli-kit/node/fs')
import {inTemporaryDirectory, writeFile, mkdir, fileExistsSync} from '@shopify/cli-kit/node/fs'
import {joinPath} from '@shopify/cli-kit/node/path'

describe('copyByPattern', () => {
let mockStdout: any
Expand All @@ -12,141 +11,166 @@ describe('copyByPattern', () => {
})

test('copies matched files preserving relative paths', async () => {
// Given
vi.mocked(fs.glob).mockResolvedValue(['/src/components/Button.tsx', '/src/utils/helpers.ts'])
vi.mocked(fs.mkdir).mockResolvedValue()
vi.mocked(fs.copyFile).mockResolvedValue()

// When
const result = await copyByPattern(
{
sourceDir: '/src',
outputDir: '/out',
patterns: ['**/*.ts', '**/*.tsx'],
ignore: [],
},
{stdout: mockStdout},
)

// Then
expect(fs.copyFile).toHaveBeenCalledWith('/src/components/Button.tsx', '/out/components/Button.tsx')
expect(fs.copyFile).toHaveBeenCalledWith('/src/utils/helpers.ts', '/out/utils/helpers.ts')
expect(result.filesCopied).toBe(2)
expect(result.outputPaths).toEqual(expect.arrayContaining(['components/Button.tsx', 'utils/helpers.ts']))
expect(mockStdout.write).toHaveBeenCalledWith(expect.stringContaining('Included 2 file(s)'))
await inTemporaryDirectory(async (tmpDir) => {
// Given
const sourceDir = joinPath(tmpDir, 'src')
const outputDir = joinPath(tmpDir, 'out')
await mkdir(sourceDir)
await mkdir(joinPath(sourceDir, 'components'))
await mkdir(joinPath(sourceDir, 'utils'))
await writeFile(joinPath(sourceDir, 'components/Button.tsx'), 'content')
await writeFile(joinPath(sourceDir, 'utils/helpers.ts'), 'content')

// When
const result = await copyByPattern(
{
sourceDir,
outputDir,
patterns: ['**/*.ts', '**/*.tsx'],
ignore: [],
},
{stdout: mockStdout},
)

// Then
expect(fileExistsSync(joinPath(outputDir, 'components/Button.tsx'))).toBe(true)
expect(fileExistsSync(joinPath(outputDir, 'utils/helpers.ts'))).toBe(true)
expect(result.filesCopied).toBe(2)
expect(result.outputPaths).toEqual(expect.arrayContaining(['components/Button.tsx', 'utils/helpers.ts']))
expect(mockStdout.write).toHaveBeenCalledWith(expect.stringContaining('Included 2 file(s)'))
})
})

test('returns 0 when no files match patterns', async () => {
// Given
vi.mocked(fs.glob).mockResolvedValue([])

// When
const result = await copyByPattern(
{
sourceDir: '/src',
outputDir: '/out',
patterns: ['**/*.png'],
ignore: [],
},
{stdout: mockStdout},
)

// Then
expect(result.filesCopied).toBe(0)
expect(result.outputPaths).toEqual([])
expect(fs.mkdir).not.toHaveBeenCalled()
expect(fs.copyFile).not.toHaveBeenCalled()
await inTemporaryDirectory(async (tmpDir) => {
// Given
const sourceDir = joinPath(tmpDir, 'src')
const outputDir = joinPath(tmpDir, 'out')
await mkdir(sourceDir)
await writeFile(joinPath(sourceDir, 'style.css'), 'content')

// When
const result = await copyByPattern(
{
sourceDir,
outputDir,
patterns: ['**/*.png'],
ignore: [],
},
{stdout: mockStdout},
)

// Then
expect(result.filesCopied).toBe(0)
expect(result.outputPaths).toEqual([])
expect(fileExistsSync(outputDir)).toBe(false)
})
})

test('skips file and warns when resolved destination escapes the output directory', async () => {
// Given — sourceDir is /out/sub, so a file from /out/sub/../../evil resolves outside /out/sub
// Simulate by providing a glob result whose relative path traverses upward
vi.mocked(fs.glob).mockResolvedValue(['/out/sub/../../evil.js'])
vi.mocked(fs.mkdir).mockResolvedValue()
vi.mocked(fs.copyFile).mockResolvedValue()

// When
const result = await copyByPattern(
{
sourceDir: '/out/sub',
outputDir: '/out/sub',
patterns: ['**/*'],
ignore: [],
},
{stdout: mockStdout},
)

// Then
expect(mockStdout.write).toHaveBeenCalledWith(expect.stringContaining('skipping'))
expect(fs.copyFile).not.toHaveBeenCalled()
expect(result.filesCopied).toBe(0)
expect(result.outputPaths).toEqual([])
await inTemporaryDirectory(async (tmpDir) => {
// Given
const sourceDir = joinPath(tmpDir, 'src')
const outputDir = joinPath(tmpDir, 'out')
const otherDir = joinPath(tmpDir, 'other')
await mkdir(sourceDir)
await mkdir(otherDir)
await writeFile(joinPath(otherDir, 'evil.js'), 'content')

// When
const result = await copyByPattern(
{
sourceDir,
outputDir,
patterns: [joinPath(otherDir, 'evil.js')],
ignore: [],
},
{stdout: mockStdout},
)

// Then
expect(mockStdout.write).toHaveBeenCalledWith(expect.stringContaining('skipping'))
expect(result.filesCopied).toBe(0)
expect(result.outputPaths).toEqual([])
expect(fileExistsSync(joinPath(outputDir, 'evil.js'))).toBe(false)
})
})

test('returns 0 without copying when filepath equals computed destPath', async () => {
// Given — file already lives at the exact destination path
vi.mocked(fs.glob).mockResolvedValue(['/out/logo.png'])
vi.mocked(fs.mkdir).mockResolvedValue()
vi.mocked(fs.copyFile).mockResolvedValue()

// When — sourceDir is /out so relPath=relativePath('/out','/out/logo.png')='logo.png', destPath='/out/logo.png'==filepath
const result = await copyByPattern(
{
sourceDir: '/out',
outputDir: '/out',
patterns: ['*.png'],
ignore: [],
},
{stdout: mockStdout},
)

// Then
expect(fs.copyFile).not.toHaveBeenCalled()
expect(result.filesCopied).toBe(0)
expect(result.outputPaths).toEqual([])
expect(mockStdout.write).toHaveBeenCalledWith(expect.stringContaining('Included 0 file(s)'))
await inTemporaryDirectory(async (tmpDir) => {
// Given — file already lives at the exact destination path
const sourceDir = tmpDir
const outputDir = tmpDir
await writeFile(joinPath(tmpDir, 'logo.png'), 'content')

// When
const result = await copyByPattern(
{
sourceDir,
outputDir,
patterns: ['*.png'],
ignore: [],
},
{stdout: mockStdout},
)

// Then
expect(result.filesCopied).toBe(0)
expect(result.outputPaths).toEqual([])
expect(mockStdout.write).toHaveBeenCalledWith(expect.stringContaining('Included 0 file(s)'))
})
})

test('calls mkdir(outputDir) before copying when files are matched', async () => {
// Given
vi.mocked(fs.glob).mockResolvedValue(['/src/app.js'])
vi.mocked(fs.mkdir).mockResolvedValue()
vi.mocked(fs.copyFile).mockResolvedValue()

// When
await copyByPattern(
{
sourceDir: '/src',
outputDir: '/out/dist',
patterns: ['*.js'],
ignore: [],
},
{stdout: mockStdout},
)

// Then — outputDir created before copying
expect(fs.mkdir).toHaveBeenCalledWith('/out/dist')
await inTemporaryDirectory(async (tmpDir) => {
// Given
const sourceDir = joinPath(tmpDir, 'src')
const outputDir = joinPath(tmpDir, 'out/dist')
await mkdir(sourceDir)
await writeFile(joinPath(sourceDir, 'app.js'), 'content')

// When
await copyByPattern(
{
sourceDir,
outputDir,
patterns: ['*.js'],
ignore: [],
},
{stdout: mockStdout},
)

// Then
expect(fileExistsSync(outputDir)).toBe(true)
expect(fileExistsSync(joinPath(outputDir, 'app.js'))).toBe(true)
})
})

test('passes ignore patterns to glob', async () => {
// Given
vi.mocked(fs.glob).mockResolvedValue([])

// When
await copyByPattern(
{
sourceDir: '/src',
outputDir: '/out',
patterns: ['**/*'],
ignore: ['**/*.test.ts', 'node_modules/**'],
},
{stdout: mockStdout},
)

// Then
expect(fs.glob).toHaveBeenCalledWith(
['**/*'],
expect.objectContaining({ignore: ['**/*.test.ts', 'node_modules/**']}),
)
await inTemporaryDirectory(async (tmpDir) => {
// Given
const sourceDir = joinPath(tmpDir, 'src')
const outputDir = joinPath(tmpDir, 'out')
await mkdir(sourceDir)
await writeFile(joinPath(sourceDir, 'app.js'), 'content')
await writeFile(joinPath(sourceDir, 'app.test.js'), 'content')

// When
const result = await copyByPattern(
{
sourceDir,
outputDir,
patterns: ['**/*'],
ignore: ['**/*.test.js'],
},
{stdout: mockStdout},
)

// Then
expect(result.filesCopied).toBe(1)
expect(result.outputPaths).toEqual(['app.js'])
expect(fileExistsSync(joinPath(outputDir, 'app.js'))).toBe(true)
expect(fileExistsSync(joinPath(outputDir, 'app.test.js'))).toBe(false)
})
})
})
Loading