diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dd8914c..73e6822 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,27 +21,28 @@ jobs: node-version-file: .tool-versions cache: npm - - name: Install dependencies - run: npm i + - name: Clean install dependencies + run: npm ci - name: Lint run: yarn lint test: - runs-on: ubuntu-22.04 - timeout-minutes: 10 + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + node-version: [16.x, 18.x, 20.x] + runs-on: ${{ matrix.os }} steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup node + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: - node-version-file: .tool-versions - cache: npm + node-version: ${{ matrix.node-version }} - - name: Install dependencies - run: npm i + - name: Clean install dependencies + run: npm ci - - name: Test - run: npm test + - run: npm run test-coverage -- --reporter=verbose + env: + CI_OS: ${{ runner.os }} diff --git a/src/core/packager.ts b/src/core/packager.ts index 4387fbe..943fa9e 100644 --- a/src/core/packager.ts +++ b/src/core/packager.ts @@ -5,6 +5,7 @@ import { processFile as defaultProcessFile } from '../utils/fileHandler.js'; import { getGitignorePatterns as defaultGetGitignorePatterns, createIgnoreFilter as defaultCreateIgnoreFilter, + IgnoreFilter, } from '../utils/gitignoreUtils.js'; import { generateOutput as defaultGenerateOutput } from './outputGenerator.js'; import { defaultIgnoreList } from '../utils/defaultIgnore.js'; @@ -64,7 +65,7 @@ async function packDirectory( dir: string, relativePath: string, config: RepopackConfigMerged, - ignoreFilter: (path: string) => boolean, + ignoreFilter: IgnoreFilter, deps: Dependencies, ): Promise<{ path: string; content: string }[]> { const entries = await fs.readdir(dir, { withFileTypes: true }); diff --git a/src/utils/gitignoreUtils.ts b/src/utils/gitignoreUtils.ts index 9fe65cc..246d04f 100644 --- a/src/utils/gitignoreUtils.ts +++ b/src/utils/gitignoreUtils.ts @@ -20,7 +20,9 @@ export function parseGitignoreContent(content: string): string[] { .filter((line) => line && !line.startsWith('#')); } -export function createIgnoreFilter(patterns: string[]): (path: string) => boolean { +export type IgnoreFilter = (path: string) => boolean; + +export function createIgnoreFilter(patterns: string[]): IgnoreFilter { const ig = ignore.default().add(patterns); return (filePath: string) => !ig.ignores(filePath); } diff --git a/tests/utils/gitignoreUtils.test.ts b/tests/utils/gitignoreUtils.test.ts index 48b7591..78c6230 100644 --- a/tests/utils/gitignoreUtils.test.ts +++ b/tests/utils/gitignoreUtils.test.ts @@ -2,58 +2,178 @@ import { expect, test, vi, describe, beforeEach } from 'vitest'; import { getGitignorePatterns, parseGitignoreContent, createIgnoreFilter } from '../../src/utils/gitignoreUtils.js'; import path from 'path'; import * as fs from 'fs/promises'; +import os from 'os'; vi.mock('fs/promises'); +const isWindows = os.platform() === 'win32'; + describe('gitignoreUtils', () => { beforeEach(() => { vi.resetAllMocks(); }); - test('getGitignorePatterns should read and parse .gitignore file', async () => { - const mockContent = ` + describe('getGitignorePatterns', () => { + test('should read and parse .gitignore file', async () => { + const mockContent = ` # Comment node_modules *.log .DS_Store - `; - vi.mocked(fs.readFile).mockResolvedValue(mockContent); + `; + vi.mocked(fs.readFile).mockResolvedValue(mockContent); - const patterns = await getGitignorePatterns('/mock/root'); + const patterns = await getGitignorePatterns('/mock/root'); - expect(fs.readFile).toHaveBeenCalledWith(path.join('/mock/root', '.gitignore'), 'utf-8'); - expect(patterns).toEqual(['node_modules', '*.log', '.DS_Store']); - }); + expect(fs.readFile).toHaveBeenCalledWith(path.join('/mock/root', '.gitignore'), 'utf-8'); + expect(patterns).toEqual(['node_modules', '*.log', '.DS_Store']); + }); + + test('should return empty array if .gitignore is not found', async () => { + vi.mocked(fs.readFile).mockRejectedValue(new Error('File not found')); + + const patterns = await getGitignorePatterns('/mock/root'); - test('getGitignorePatterns should return empty array if .gitignore is not found', async () => { - vi.mocked(fs.readFile).mockRejectedValue(new Error('File not found')); + expect(patterns).toEqual([]); + }); - const patterns = await getGitignorePatterns('/mock/root'); + test('should handle CRLF line endings', async () => { + const mockContent = 'node_modules\r\n*.log\r\n.DS_Store'; + vi.mocked(fs.readFile).mockResolvedValue(mockContent); - expect(patterns).toEqual([]); + const patterns = await getGitignorePatterns('/mock/root'); + + expect(patterns).toEqual(['node_modules', '*.log', '.DS_Store']); + }); }); - test('parseGitignoreContent should correctly parse gitignore content', () => { - const content = ` + describe('parseGitignoreContent', () => { + test('should correctly parse gitignore content', () => { + const content = ` # Comment node_modules *.log .DS_Store - `; + `; + + const patterns = parseGitignoreContent(content); + + expect(patterns).toEqual(['node_modules', '*.log', '.DS_Store']); + }); + + test('should handle mixed line endings', () => { + const content = 'node_modules\n*.log\r\n.DS_Store\r'; - const patterns = parseGitignoreContent(content); + const patterns = parseGitignoreContent(content); - expect(patterns).toEqual(['node_modules', '*.log', '.DS_Store']); + expect(patterns).toEqual(['node_modules', '*.log', '.DS_Store']); + }); }); - test('createIgnoreFilter should create a function that correctly filters paths', () => { - const patterns = ['node_modules', '*.log', '.DS_Store']; - const filter = createIgnoreFilter(patterns); + describe('createIgnoreFilter', () => { + test('should create a function that correctly filters paths', () => { + const patterns = ['node_modules', '*.log', '.DS_Store']; + const filter = createIgnoreFilter(patterns); + + expect(filter('src/index.js')).toBe(true); + expect(filter('node_modules/package/index.js')).toBe(false); + expect(filter('logs/error.log')).toBe(false); + expect(filter('.DS_Store')).toBe(false); + }); + + test('should correctly ignore files with different path separators', () => { + const patterns = ['*.md', '*.svg', '*.css', 'node_modules/**']; + const filter = createIgnoreFilter(patterns); + + // UNIX-style paths + expect(filter('README.md')).toBe(false); + expect(filter('src/assets/logo.svg')).toBe(false); + expect(filter('styles/main.css')).toBe(false); + expect(filter('node_modules/package/index.js')).toBe(false); + + // Files that should not be ignored + expect(filter('src/index.js')).toBe(true); + }); + + test.runIf(isWindows)('should correctly ignore files with Windows-style paths', () => { + const patterns = ['*.md', '*.svg', '*.css', 'node_modules/**']; + const filter = createIgnoreFilter(patterns); + + expect(filter('docs\\README.md')).toBe(false); + expect(filter('src\\assets\\logo.svg')).toBe(false); + expect(filter('styles\\main.css')).toBe(false); + expect(filter('node_modules\\package\\index.js')).toBe(false); + expect(filter('src\\components\\Button.js')).toBe(true); + }); + + test('should handle nested directory patterns correctly', () => { + const patterns = ['test/**/*.spec.js', 'build/**']; + const filter = createIgnoreFilter(patterns); + + expect(filter('test/unit/component.spec.js')).toBe(false); + expect(filter('build/output.js')).toBe(false); + + expect(filter('src/test/helper.js')).toBe(true); + }); + + test.runIf(isWindows)('should handle nested directory patterns with Windows-style paths', () => { + const patterns = ['test/**/*.spec.js', 'build/**']; + const filter = createIgnoreFilter(patterns); + + expect(filter('src\\build\\utils.js')).toBe(true); + expect(filter('test\\integration\\api.spec.js')).toBe(false); + expect(filter('build\\temp\\cache.json')).toBe(false); + }); + + test('should correctly handle patterns with special characters', () => { + const patterns = ['**/*.min.js', '**/#temp#', '**/node_modules']; + const filter = createIgnoreFilter(patterns); + + expect(filter('dist/bundle.min.js')).toBe(false); + expect(filter('temp/#temp#/file.txt')).toBe(false); + expect(filter('project/node_modules/package/index.js')).toBe(false); + + expect(filter('src/app.js')).toBe(true); + expect(filter('docs/temp/file.txt')).toBe(true); + }); + + test('should handle case sensitivity correctly', () => { + const patterns = ['*.MD', 'TEST']; + const filter = createIgnoreFilter(patterns); + + expect(filter('readme.md')).toBe(false); + expect(filter('test/file.txt')).toBe(false); + + expect(filter('README.MD')).toBe(false); + expect(filter('TEST/file.txt')).toBe(false); + }); + + test('should handle symlinks correctly', () => { + const patterns = ['symlink', 'real_dir']; + const filter = createIgnoreFilter(patterns); + + expect(filter('symlink')).toBe(false); + expect(filter('real_dir')).toBe(false); + expect(filter('symlink/file.txt')).toBe(false); + expect(filter('real_dir/file.txt')).toBe(false); + }); + + test('should handle long paths correctly', () => { + const longPath = 'a'.repeat(200) + '/file.txt'; + const patterns = ['**/*.txt']; + const filter = createIgnoreFilter(patterns); + + expect(filter(longPath)).toBe(false); + }); + + test('should handle Unicode characters in paths and patterns', () => { + const patterns = ['📁/*', '*.🚀']; + const filter = createIgnoreFilter(patterns); - expect(filter('src/index.js')).toBe(true); - expect(filter('node_modules/package/index.js')).toBe(false); - expect(filter('logs/error.log')).toBe(false); - expect(filter('.DS_Store')).toBe(false); + expect(filter('📁/file.txt')).toBe(false); + expect(filter('document.🚀')).toBe(false); + expect(filter('normal/path/file.txt')).toBe(true); + }); }); }); diff --git a/vite.config.mts b/vite.config.mts index e75dd23..13ab703 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -6,6 +6,7 @@ export default defineConfig({ environment: 'node', include: ['tests/**/*.test.ts'], coverage: { + include: ['src/**/*'], reporter: ['text', 'json', 'html'], }, }