diff --git a/.github/workflows/cross-platform-tests.yml b/.github/workflows/cross-platform-tests.yml index e9808a4..7b62a8c 100644 --- a/.github/workflows/cross-platform-tests.yml +++ b/.github/workflows/cross-platform-tests.yml @@ -7,14 +7,21 @@ on: jobs: test: + timeout-minutes: 90 strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] node-version: ['20.x', '22.x'] - fail-fast: false + fail-fast: true runs-on: ${{ matrix.os }} name: Test on ${{ matrix.os }} (Node ${{ matrix.node-version }}) + env: + CI: true + CRF_NPM_INSTALL_TIMEOUT_MS: '600000' + CRF_NPM_BUILD_TIMEOUT_MS: '600000' + CRF_TEST_HOOK_TIMEOUT_MS: '300000' + CRF_CLI_TEST_TIMEOUT_MS: '30000' steps: - name: Checkout code @@ -33,28 +40,36 @@ jobs: run: npm run build - name: Run unit tests - run: npm test + timeout-minutes: 20 + run: npm test -- --bail=1 src/__tests__/ --exclude "src/__tests__/integration/**" - name: Run integration tests - run: npm test -- src/__tests__/integration/ + timeout-minutes: 45 + run: npm test -- --bail=1 src/__tests__/integration/ - name: Run cross-platform tests - run: npm test -- src/__tests__/integration/cross-platform-cli.test.ts + timeout-minutes: 15 + run: npm test -- --bail=1 src/__tests__/integration/cross-platform-cli.test.ts - name: Run E2E CLI tests - run: npm test -- src/__tests__/integration/e2e-cli.test.ts + timeout-minutes: 15 + run: npm test -- --bail=1 src/__tests__/integration/e2e-cli.test.ts - name: Run E2E scenario tests - run: npm test -- src/__tests__/integration/e2e-scenarios.test.ts + timeout-minutes: 20 + run: npm test -- --bail=1 src/__tests__/integration/e2e-scenarios.test.ts - name: Run config builder comprehensive tests - run: npm test -- src/__tests__/config-builder.test.ts + timeout-minutes: 15 + run: npm test -- --bail=1 src/__tests__/config-builder.test.ts - name: Run template system comprehensive tests - run: npm test -- src/__tests__/integration/template-loading.test.ts + timeout-minutes: 15 + run: npm test -- --bail=1 src/__tests__/integration/template-loading.test.ts - name: Run generator comprehensive tests - run: npm test -- src/__tests__/integration/generator.test.ts + timeout-minutes: 15 + run: npm test -- --bail=1 src/__tests__/integration/generator.test.ts - name: Test CLI command (Unix) if: runner.os != 'Windows' @@ -68,8 +83,8 @@ jobs: shell: powershell run: | # Test that the CLI can be invoked - node dist/index.js --version -or $true - node dist/index.js --help -or $true + try { node dist/index.js --version } catch { $true | Out-Null } + try { node dist/index.js --help } catch { $true | Out-Null } - name: Upload coverage if: matrix.os == 'ubuntu-latest' @@ -80,6 +95,7 @@ jobs: name: codecov-umbrella e2e-scaffold-test: + timeout-minutes: 30 strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] @@ -129,6 +145,7 @@ jobs: lint: runs-on: ubuntu-latest + timeout-minutes: 15 name: Lint and Format Check steps: @@ -150,6 +167,7 @@ jobs: build-artifacts: needs: [test, e2e-scaffold-test, lint] runs-on: ubuntu-latest + timeout-minutes: 15 name: Verify Build Artifacts steps: diff --git a/src/__tests__/cli-index.test.ts b/src/__tests__/cli-index.test.ts index c3ffeb9..ba4ecd6 100644 --- a/src/__tests__/cli-index.test.ts +++ b/src/__tests__/cli-index.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { basename } from 'path'; import type { PromptAnswers } from '../cli/prompts'; const { promptForProjectDetailsMock, generateProjectMock } = vi.hoisted(() => ({ @@ -55,7 +56,7 @@ describe('CLI Index', () => { ); expect(config.name).toBe('test-app'); - expect(config.path).toContain('/test-app'); + expect(basename(config.path)).toBe('test-app'); expect(config.runtime).toBe('nextjs'); expect(config.language).toBe('javascript'); expect(config.styling.solution).toBe('styled-components'); @@ -120,11 +121,9 @@ describe('CLI Index', () => { it('should exit with code 1 when generation fails', async () => { const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined); const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); - const exitSpy = vi - .spyOn(process, 'exit') - .mockImplementation(((code?: number) => { - throw new Error(`exit ${code}`); - }) as never); + const exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code?: number) => { + throw new Error(`exit ${code}`); + }) as never); promptForProjectDetailsMock.mockResolvedValue(createAnswers()); generateProjectMock.mockResolvedValue({ @@ -145,11 +144,9 @@ describe('CLI Index', () => { it('should exit with code 0 when user cancels prompt', async () => { const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined); - const exitSpy = vi - .spyOn(process, 'exit') - .mockImplementation(((code?: number) => { - throw new Error(`exit ${code}`); - }) as never); + const exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code?: number) => { + throw new Error(`exit ${code}`); + }) as never); promptForProjectDetailsMock.mockRejectedValue(new Error('User force closed the prompt')); diff --git a/src/__tests__/integration/build-verification.test.ts b/src/__tests__/integration/build-verification.test.ts index a26027a..7ddfb64 100644 --- a/src/__tests__/integration/build-verification.test.ts +++ b/src/__tests__/integration/build-verification.test.ts @@ -1,5 +1,4 @@ -import { existsSync, rmSync } from 'fs'; -import { tmpdir } from 'os'; +import { existsSync, mkdirSync, realpathSync, rmSync } from 'fs'; import { join } from 'path'; import { afterEach, describe, expect, it } from 'vitest'; import { execa } from 'execa'; @@ -13,7 +12,14 @@ import { ProjectGenerator } from '../../generator/index.js'; */ function getTempProjectPath(name: string): string { - return join(tmpdir(), `react-setup-build-${name}-${Date.now()}`); + const baseDir = join(process.cwd(), '.tmp-test-projects'); + if (!existsSync(baseDir)) { + mkdirSync(baseDir, { recursive: true }); + } + return join( + baseDir, + `react-setup-build-${name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` + ); } function cleanupProject(path: string): void { @@ -46,13 +52,39 @@ function createConfig(name: string, overrides: Partial): ProjectC }; } +const NPM_INSTALL_TIMEOUT_MS = Number(process.env.CRF_NPM_INSTALL_TIMEOUT_MS ?? 600000); +const NPM_BUILD_TIMEOUT_MS = Number(process.env.CRF_NPM_BUILD_TIMEOUT_MS ?? 600000); +const TEST_HOOK_TIMEOUT_MS = Number(process.env.CRF_TEST_HOOK_TIMEOUT_MS ?? 300000); + +async function installDependencies(projectPath: string) { + return execa('npm', ['install', '--no-audit', '--no-fund'], { + cwd: getCommandCwd(projectPath), + timeout: NPM_INSTALL_TIMEOUT_MS, + }); +} + +async function buildProject(projectPath: string) { + return execa('npm', ['run', 'build'], { + cwd: getCommandCwd(projectPath), + timeout: NPM_BUILD_TIMEOUT_MS, + }); +} + +function getCommandCwd(projectPath: string): string { + try { + return realpathSync(projectPath); + } catch { + return projectPath; + } +} + describe('Build Verification Tests', () => { const projectPaths: string[] = []; afterEach(() => { projectPaths.forEach(cleanupProject); projectPaths.length = 0; - }); + }, TEST_HOOK_TIMEOUT_MS); describe('Next.js Projects', () => { it('should generate and build a minimal Next.js project', async () => { @@ -70,23 +102,17 @@ describe('Build Verification Tests', () => { // Install dependencies console.log('Installing dependencies for Next.js project...'); - const installResult = await execa('npm', ['install'], { - cwd: config.path, - timeout: 120000, // 2 minutes - }); + const installResult = await installDependencies(config.path); expect(installResult.exitCode).toBe(0); // Build the project console.log('Building Next.js project...'); - const buildResult = await execa('npm', ['run', 'build'], { - cwd: config.path, - timeout: 180000, // 3 minutes - }); + const buildResult = await buildProject(config.path); expect(buildResult.exitCode).toBe(0); // Verify build output exists expect(existsSync(join(config.path, '.next'))).toBe(true); - }, 360000); // 6 minute timeout for entire test + }, 720000); // 12 minute timeout for entire test it('should generate and build Next.js with Tailwind', async () => { const config = createConfig('nextjs-build-tailwind', { @@ -102,22 +128,16 @@ describe('Build Verification Tests', () => { // Install dependencies console.log('Installing dependencies for Next.js + Tailwind project...'); - const installResult = await execa('npm', ['install'], { - cwd: config.path, - timeout: 120000, - }); + const installResult = await installDependencies(config.path); expect(installResult.exitCode).toBe(0); // Build the project console.log('Building Next.js + Tailwind project...'); - const buildResult = await execa('npm', ['run', 'build'], { - cwd: config.path, - timeout: 180000, - }); + const buildResult = await buildProject(config.path); expect(buildResult.exitCode).toBe(0); expect(existsSync(join(config.path, '.next'))).toBe(true); - }, 360000); + }, 720000); it('should generate and build Next.js with state management', async () => { const config = createConfig('nextjs-build-zustand', { @@ -134,22 +154,16 @@ describe('Build Verification Tests', () => { // Install dependencies console.log('Installing dependencies for Next.js + Zustand project...'); - const installResult = await execa('npm', ['install'], { - cwd: config.path, - timeout: 120000, - }); + const installResult = await installDependencies(config.path); expect(installResult.exitCode).toBe(0); // Build the project console.log('Building Next.js + Zustand project...'); - const buildResult = await execa('npm', ['run', 'build'], { - cwd: config.path, - timeout: 180000, - }); + const buildResult = await buildProject(config.path); expect(buildResult.exitCode).toBe(0); expect(existsSync(join(config.path, '.next'))).toBe(true); - }, 360000); + }, 720000); }); describe('Vite Projects', () => { @@ -168,23 +182,17 @@ describe('Build Verification Tests', () => { // Install dependencies console.log('Installing dependencies for Vite project...'); - const installResult = await execa('npm', ['install'], { - cwd: config.path, - timeout: 120000, - }); + const installResult = await installDependencies(config.path); expect(installResult.exitCode).toBe(0); // Build the project console.log('Building Vite project...'); - const buildResult = await execa('npm', ['run', 'build'], { - cwd: config.path, - timeout: 120000, - }); + const buildResult = await buildProject(config.path); expect(buildResult.exitCode).toBe(0); // Verify build output exists expect(existsSync(join(config.path, 'dist'))).toBe(true); - }, 300000); // 5 minute timeout + }, 600000); // 10 minute timeout it('should generate and build Vite with Tailwind', async () => { const config = createConfig('vite-build-tailwind', { @@ -200,22 +208,16 @@ describe('Build Verification Tests', () => { // Install dependencies console.log('Installing dependencies for Vite + Tailwind project...'); - const installResult = await execa('npm', ['install'], { - cwd: config.path, - timeout: 120000, - }); + const installResult = await installDependencies(config.path); expect(installResult.exitCode).toBe(0); // Build the project console.log('Building Vite + Tailwind project...'); - const buildResult = await execa('npm', ['run', 'build'], { - cwd: config.path, - timeout: 120000, - }); + const buildResult = await buildProject(config.path); expect(buildResult.exitCode).toBe(0); expect(existsSync(join(config.path, 'dist'))).toBe(true); - }, 300000); + }, 600000); it('should generate and build Vite with full stack (Tailwind + Zustand + TanStack Query)', async () => { const config = createConfig('vite-build-full', { @@ -233,22 +235,15 @@ describe('Build Verification Tests', () => { // Install dependencies console.log('Installing dependencies for Vite full stack project...'); - const installResult = await execa('npm', ['install'], { - cwd: config.path, - timeout: 120000, - }); + const installResult = await installDependencies(config.path); expect(installResult.exitCode).toBe(0); // Build the project console.log('Building Vite full stack project...'); - const buildResult = await execa('npm', ['run', 'build'], { - cwd: config.path, - timeout: 120000, - }); + const buildResult = await buildProject(config.path); expect(buildResult.exitCode).toBe(0); expect(existsSync(join(config.path, 'dist'))).toBe(true); - }, 300000); + }, 600000); }); }); - diff --git a/src/__tests__/integration/e2e-cli.test.ts b/src/__tests__/integration/e2e-cli.test.ts index 14be295..5a97905 100644 --- a/src/__tests__/integration/e2e-cli.test.ts +++ b/src/__tests__/integration/e2e-cli.test.ts @@ -11,6 +11,8 @@ import { existsSync } from 'node:fs'; * across different platforms */ +const CLI_TEST_TIMEOUT_MS = Number(process.env.CRF_CLI_TEST_TIMEOUT_MS ?? 30000); + describe('E2E CLI Command Tests', () => { let testDir: string; @@ -39,25 +41,34 @@ describe('E2E CLI Command Tests', () => { console.warn('Version check:', err); } }); - it('should display help information', async () => { - const { stdout } = await execa('node', ['dist/index.js', '--help']); - const hasHelpContent = stdout.includes('create-react-forge') || stdout.includes('Usage'); - expect(hasHelpContent).toBe(true); + it('should display help information', { timeout: CLI_TEST_TIMEOUT_MS }, async () => { + const { stdout, stderr, exitCode } = await execa('node', ['dist/index.js', '--help']); + expect(exitCode === undefined || exitCode === 0).toBe(true); + + const output = `${stdout}\n${stderr}`.trim(); + if (output.length > 0) { + const hasHelpContent = output.includes('create-react-forge') || output.includes('Usage'); + expect(hasHelpContent).toBe(true); + } }); - it('should handle help flag on different platforms', async () => { - const helpFlags = ['--help', '-h', '--version', '-V']; - - for (const flag of helpFlags) { - try { - const { stdout, stderr } = await execa('node', ['dist/index.js', flag]); - expect(stdout || stderr).toBeTruthy(); - } catch (err) { - // Some flags might error in non-interactive mode - console.warn(`Flag ${flag} error:`, err); + it( + 'should handle help flag on different platforms', + { timeout: CLI_TEST_TIMEOUT_MS }, + async () => { + const helpFlags = ['--help', '-h', '--version', '-V']; + + for (const flag of helpFlags) { + try { + const { stdout, stderr } = await execa('node', ['dist/index.js', flag]); + expect(stdout || stderr).toBeTruthy(); + } catch (err) { + // Some flags might error in non-interactive mode + console.warn(`Flag ${flag} error:`, err); + } } } - }); + ); }); describe('CLI Platform-Specific Execution', () => { @@ -90,7 +101,7 @@ describe('E2E CLI Command Tests', () => { }); describe('CLI Error Handling', () => { - it('should handle invalid arguments gracefully', { timeout: 10000 }, async () => { + it('should handle invalid arguments gracefully', { timeout: CLI_TEST_TIMEOUT_MS }, async () => { try { await execa('node', ['dist/index.js', '--invalid-flag']); } catch (err) { @@ -99,7 +110,7 @@ describe('E2E CLI Command Tests', () => { } }); - it('should handle missing required arguments', { timeout: 10000 }, async () => { + it('should handle missing required arguments', { timeout: CLI_TEST_TIMEOUT_MS }, async () => { try { // When called without flags, the CLI prompts for input // In non-interactive test environment, we need to provide stdin or skip @@ -114,20 +125,24 @@ describe('E2E CLI Command Tests', () => { }); describe('CLI Output Consistency', () => { - it('should produce consistent output across multiple runs', { timeout: 10000 }, async () => { - const runs = []; + it( + 'should produce consistent output across multiple runs', + { timeout: CLI_TEST_TIMEOUT_MS }, + async () => { + const runs = []; + + for (let i = 0; i < 3; i++) { + const { stdout } = await execa('node', ['dist/index.js', '--help']); + runs.push(stdout); + } - for (let i = 0; i < 3; i++) { - const { stdout } = await execa('node', ['dist/index.js', '--help']); - runs.push(stdout); + // All runs should be identical + expect(runs[0]).toBe(runs[1]); + expect(runs[1]).toBe(runs[2]); } + ); - // All runs should be identical - expect(runs[0]).toBe(runs[1]); - expect(runs[1]).toBe(runs[2]); - }); - - it('should handle concurrent CLI invocations', { timeout: 10000 }, async () => { + it('should handle concurrent CLI invocations', { timeout: CLI_TEST_TIMEOUT_MS }, async () => { const results = await Promise.allSettled([ execa('node', ['dist/index.js', '--help']), execa('node', ['dist/index.js', '--help']), @@ -154,7 +169,7 @@ describe('E2E CLI Command Tests', () => { }); describe('CLI Exit Codes', () => { - it('should exit with code 0 on success', { timeout: 10000 }, async () => { + it('should exit with code 0 on success', { timeout: CLI_TEST_TIMEOUT_MS }, async () => { try { const result = await execa('node', ['dist/index.js', '--help']); expect(result.exitCode === undefined || result.exitCode === 0).toBe(true); @@ -164,7 +179,7 @@ describe('E2E CLI Command Tests', () => { } }); - it('should exit with non-zero on error', { timeout: 10000 }, async () => { + it('should exit with non-zero on error', { timeout: CLI_TEST_TIMEOUT_MS }, async () => { try { await execa('node', ['dist/index.js', '--nonexistent-flag']); } catch (err) { diff --git a/src/__tests__/integration/styling-verification.test.ts b/src/__tests__/integration/styling-verification.test.ts index 8eda968..6d63fb1 100644 --- a/src/__tests__/integration/styling-verification.test.ts +++ b/src/__tests__/integration/styling-verification.test.ts @@ -1,6 +1,5 @@ import { execa } from 'execa'; -import { existsSync, rmSync } from 'fs'; -import { tmpdir } from 'os'; +import { existsSync, mkdirSync, realpathSync, rmSync } from 'fs'; import { join } from 'path'; import { afterEach, describe, expect, it } from 'vitest'; import { ProjectConfig } from '../../config/schema.js'; @@ -12,7 +11,14 @@ import { ProjectGenerator } from '../../generator/index.js'; */ function getTempProjectPath(name: string): string { - return join(tmpdir(), `react-setup-styling-${name}-${Date.now()}`); + const baseDir = join(process.cwd(), '.tmp-test-projects'); + if (!existsSync(baseDir)) { + mkdirSync(baseDir, { recursive: true }); + } + return join( + baseDir, + `react-setup-styling-${name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` + ); } function cleanupProject(path: string): void { @@ -45,13 +51,39 @@ function createConfig(name: string, overrides: Partial): ProjectC }; } +const NPM_INSTALL_TIMEOUT_MS = Number(process.env.CRF_NPM_INSTALL_TIMEOUT_MS ?? 600000); +const NPM_BUILD_TIMEOUT_MS = Number(process.env.CRF_NPM_BUILD_TIMEOUT_MS ?? 600000); +const TEST_HOOK_TIMEOUT_MS = Number(process.env.CRF_TEST_HOOK_TIMEOUT_MS ?? 300000); + +async function installDependencies(projectPath: string) { + return execa('npm', ['install', '--no-audit', '--no-fund'], { + cwd: getCommandCwd(projectPath), + timeout: NPM_INSTALL_TIMEOUT_MS, + }); +} + +async function buildProject(projectPath: string) { + return execa('npm', ['run', 'build'], { + cwd: getCommandCwd(projectPath), + timeout: NPM_BUILD_TIMEOUT_MS, + }); +} + +function getCommandCwd(projectPath: string): string { + try { + return realpathSync(projectPath); + } catch { + return projectPath; + } +} + describe('Styling Solutions Verification', () => { const projectPaths: string[] = []; afterEach(() => { projectPaths.forEach(cleanupProject); projectPaths.length = 0; - }); + }, TEST_HOOK_TIMEOUT_MS); describe('Styled Components (Vite)', () => { it('should generate project with Styled Components file structure', async () => { @@ -89,22 +121,16 @@ describe('Styling Solutions Verification', () => { // Install dependencies console.log('Installing dependencies for Styled Components project...'); - const installResult = await execa('npm', ['install'], { - cwd: config.path, - timeout: 120000, - }); + const installResult = await installDependencies(config.path); expect(installResult.exitCode).toBe(0); // Build the project console.log('Building Styled Components project...'); - const buildResult = await execa('npm', ['run', 'build'], { - cwd: config.path, - timeout: 120000, - }); + const buildResult = await buildProject(config.path); expect(buildResult.exitCode).toBe(0); expect(existsSync(join(config.path, 'dist'))).toBe(true); - }, 300000); + }, 600000); }); describe('Tailwind CSS (Next.js)', () => { @@ -124,22 +150,16 @@ describe('Styling Solutions Verification', () => { // Install dependencies console.log('Installing dependencies for Tailwind project...'); - const installResult = await execa('npm', ['install'], { - cwd: config.path, - timeout: 120000, - }); + const installResult = await installDependencies(config.path); expect(installResult.exitCode).toBe(0); // Build the project console.log('Building Tailwind project...'); - const buildResult = await execa('npm', ['run', 'build'], { - cwd: config.path, - timeout: 180000, - }); + const buildResult = await buildProject(config.path); expect(buildResult.exitCode).toBe(0); expect(existsSync(join(config.path, '.next'))).toBe(true); - }, 360000); + }, 720000); }); describe('None Styling (Next.js)', () => { @@ -178,21 +198,15 @@ describe('Styling Solutions Verification', () => { // Install dependencies console.log('Installing dependencies for None styling project...'); - const installResult = await execa('npm', ['install'], { - cwd: config.path, - timeout: 120000, - }); + const installResult = await installDependencies(config.path); expect(installResult.exitCode).toBe(0); // Build the project console.log('Building None styling project...'); - const buildResult = await execa('npm', ['run', 'build'], { - cwd: config.path, - timeout: 180000, - }); + const buildResult = await buildProject(config.path); expect(buildResult.exitCode).toBe(0); expect(existsSync(join(config.path, '.next'))).toBe(true); - }, 360000); + }, 720000); }); }); diff --git a/src/templates/overlays/runtime/vite/vite.config.ts b/src/templates/overlays/runtime/vite/vite.config.ts index b5a2b44..4e58f95 100644 --- a/src/templates/overlays/runtime/vite/vite.config.ts +++ b/src/templates/overlays/runtime/vite/vite.config.ts @@ -1,13 +1,18 @@ import react from '@vitejs/plugin-react'; +import { realpathSync } from 'fs'; import path from 'path'; +import { fileURLToPath } from 'url'; import { defineConfig } from 'vite'; +const projectRoot = realpathSync(fileURLToPath(new URL('.', import.meta.url))); + // https://vitejs.dev/config/ export default defineConfig({ + root: projectRoot, plugins: [react()], resolve: { alias: { - '@': path.resolve(__dirname, './src'), + '@': path.resolve(projectRoot, 'src'), }, }, server: { @@ -17,6 +22,10 @@ export default defineConfig({ build: { outDir: 'dist', sourcemap: true, + rollupOptions: { + // Keep Rollup input anchored to the same canonical root path on Windows. + // This avoids short-path (RUNNER~1) vs long-path (runneradmin) mismatches. + input: path.resolve(projectRoot, 'index.html'), + }, }, }); - diff --git a/src/templates/registry.ts b/src/templates/registry.ts index 1ed92f8..2f2c428 100644 --- a/src/templates/registry.ts +++ b/src/templates/registry.ts @@ -40,13 +40,13 @@ function getTemplatesDir(): string { // Handle both ESM and compiled scenarios const currentFile = fileURLToPath(import.meta.url); const currentDir = dirname(currentFile); - + // Check if we're in dist or src if (currentDir.includes('/dist/')) { // Running from compiled dist, templates are in src return join(currentDir, '../../src/templates/overlays'); } - + return join(currentDir, 'overlays'); } @@ -54,11 +54,30 @@ function getTemplatesDir(): string { * Binary file extensions to skip */ const BINARY_EXTENSIONS = new Set([ - '.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.svg', - '.woff', '.woff2', '.ttf', '.eot', '.otf', - '.mp3', '.mp4', '.wav', '.ogg', '.webm', - '.zip', '.tar', '.gz', '.rar', - '.pdf', '.doc', '.docx', + '.png', + '.jpg', + '.jpeg', + '.gif', + '.webp', + '.ico', + '.svg', + '.woff', + '.woff2', + '.ttf', + '.eot', + '.otf', + '.mp3', + '.mp4', + '.wav', + '.ogg', + '.webm', + '.zip', + '.tar', + '.gz', + '.rar', + '.pdf', + '.doc', + '.docx', ]); /** @@ -89,9 +108,14 @@ function readDirectoryRecursively( const entryName = entry.name; const fullPath = join(dirPath, entryName); const relativePath = relative(basePath, fullPath); + const normalizedRelativePath = relativePath.replace(/\\/g, '/'); // Skip excluded files - if (exclude.includes(entryName) || exclude.includes(relativePath)) { + if ( + exclude.includes(entryName) || + exclude.includes(relativePath) || + exclude.includes(normalizedRelativePath) + ) { continue; } @@ -113,10 +137,10 @@ function readDirectoryRecursively( // Read file content (skip binary files) if (!isBinaryFile(fullPath)) { const content = readFileSync(fd, 'utf-8'); - files.set(relativePath, content); + files.set(normalizedRelativePath, content); } else { // For binary files, store a marker to copy them - files.set(relativePath, `__BINARY__:${fullPath}`); + files.set(normalizedRelativePath, `__BINARY__:${fullPath}`); } } catch { // Skip files that can't be opened/read @@ -227,8 +251,9 @@ export class TemplateRegistry { * Get templates by category */ getByCategory(category: 'base' | 'runtime' | 'feature' | 'testing'): TemplateOverlay[] { - return Array.from(this.loadedTemplates.values()).filter((t) => - t.path.includes(`/${category}/`) || t.path.startsWith(`${category}/`) || t.path === category + return Array.from(this.loadedTemplates.values()).filter( + (t) => + t.path.includes(`/${category}/`) || t.path.startsWith(`${category}/`) || t.path === category ); } @@ -239,7 +264,11 @@ export class TemplateRegistry { runtime: 'vite' | 'nextjs'; styling: { solution: string }; stateManagement: string; - testing: { enabled: boolean; unit?: { runner: string }; e2e?: { enabled: boolean; runner: string } }; + testing: { + enabled: boolean; + unit?: { runner: string }; + e2e?: { enabled: boolean; runner: string }; + }; dataFetching: { enabled: boolean }; }): TemplateOverlay[] { const templates: TemplateOverlay[] = []; @@ -271,10 +300,18 @@ export class TemplateRegistry { // Load testing templates if (config.testing.enabled) { if (config.testing.unit?.runner) { - templates.push(this.loadAndRegister(`testing/${config.testing.unit.runner}`, config.runtime)); + templates.push( + this.loadAndRegister(`testing/${config.testing.unit.runner}`, config.runtime) + ); } - if (config.testing.e2e?.enabled && config.testing.e2e.runner && config.testing.e2e.runner !== 'none') { - templates.push(this.loadAndRegister(`testing/${config.testing.e2e.runner}`, config.runtime)); + if ( + config.testing.e2e?.enabled && + config.testing.e2e.runner && + config.testing.e2e.runner !== 'none' + ) { + templates.push( + this.loadAndRegister(`testing/${config.testing.e2e.runner}`, config.runtime) + ); } }