diff --git a/packages/app/src/cli/services/build/steps/include-assets/copy-by-pattern.test.ts b/packages/app/src/cli/services/build/steps/include-assets/copy-by-pattern.test.ts index ea4cf85511..d3dd04c36f 100644 --- a/packages/app/src/cli/services/build/steps/include-assets/copy-by-pattern.test.ts +++ b/packages/app/src/cli/services/build/steps/include-assets/copy-by-pattern.test.ts @@ -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 @@ -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) + }) }) })