diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml index 60448d5..632b5f6 100644 --- a/.github/codeql/codeql-config.yml +++ b/.github/codeql/codeql-config.yml @@ -4,7 +4,3 @@ paths-ignore: - 'build/**' - 'coverage/**' - 'dist/**' - - 'src/renderer/bundle.js' - - 'src/renderer/bundle.js.map' - - 'src/renderer/bundle.js.LICENSE.txt' - - 'src/renderer/output.css' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bdbd3b5..98c2dfe 100755 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,6 +31,9 @@ jobs: - name: Prepare build run: node scripts/prepare-build.js windows + - name: Build CSS + run: npm run build:css + - name: Build Webpack run: npm run build:webpack @@ -101,6 +104,9 @@ jobs: fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n'); " + - name: Build CSS + run: npm run build:css + - name: Build Webpack run: npm run build:webpack @@ -142,6 +148,9 @@ jobs: - name: Prepare build run: node scripts/prepare-build.js mac + - name: Build CSS + run: npm run build:css + - name: Build Webpack run: npm run build:webpack diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 3dd4e98..9926eb2 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -81,7 +81,7 @@ jobs: -Dsonar.sources=src -Dsonar.tests=tests,src/__tests__ -Dsonar.test.inclusions=tests/**/*.test.{js,jsx,ts,tsx},tests/**/*.spec.{js,jsx,ts,tsx},tests/**/*.stress.test.{js,jsx,ts,tsx},src/**/__tests__/**/*.{js,jsx,ts,tsx},src/**/*.test.{js,jsx,ts,tsx},src/**/*.spec.{js,jsx,ts,tsx} - -Dsonar.exclusions=node_modules/**,dist/**,coverage/**,tests/**,scripts/**,**/__tests__/**,**/*.test.js,**/*.test.jsx,**/*.test.ts,**/*.test.tsx,**/*.spec.js,**/*.spec.jsx,**/*.spec.ts,**/*.spec.tsx,src/renderer/bundle.js,src/renderer/bundle.js.map,src/renderer/bundle.js.LICENSE.txt,src/renderer/output.css,src/renderer/styles.css + -Dsonar.exclusions=node_modules/**,dist/**,coverage/**,tests/**,scripts/**,**/__tests__/**,**/*.test.js,**/*.test.jsx,**/*.test.ts,**/*.test.tsx,**/*.spec.js,**/*.spec.jsx,**/*.spec.ts,**/*.spec.tsx,src/renderer/styles.css -Dsonar.cpd.exclusions=tests/**,scripts/**,src/**/__tests__/**,**/*.test.js,**/*.test.jsx,**/*.test.ts,**/*.test.tsx,**/*.spec.js,**/*.spec.jsx,**/*.spec.ts,**/*.spec.tsx,**/*.stress.test.js,**/*.stress.test.jsx,**/*.stress.test.ts,**/*.stress.test.tsx - name: SonarCloud quality gate diff --git a/.gitignore b/.gitignore index 34de8d7..bca745d 100755 --- a/.gitignore +++ b/.gitignore @@ -15,9 +15,6 @@ /build # Build outputs -/src/renderer/index.js -/src/renderer/index.js.map -/src/renderer/output.css /.tmp* /temp @@ -70,9 +67,6 @@ $RECYCLE.BIN/ start/ start/* /signed/ -src/renderer/bundle.js.LICENSE.txt -src/renderer/bundle.js.map -src/renderer/bundle.js # Test artifacts /test-results diff --git a/package.json b/package.json index efa435c..02a487f 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "watch:webpack": "cross-env NODE_ENV=development webpack --mode development --watch", "predev": "npm run build:ts && node scripts/clean-dev-assets.js", "dev": "node scripts/index.js dev", - "clear-assets": "rimraf src/renderer/bundle.js src/renderer/bundle.js.map src/renderer/bundle.js.LICENSE.txt src/renderer/output.css", + "clear-assets": "rimraf dist/renderer/bundle.js dist/renderer/bundle.js.map dist/renderer/bundle.js.LICENSE.txt dist/renderer/output.css", "lint": "npm run format:check && eslint src tests scripts eslint.config.js .eslintrc.js .babelrc.js babel.config.js jest.config.js playwright.config.ts postcss.config.js prettier.config.js tailwind.config.js webpack.config.js --cache --max-warnings 0 && npm run lint:md && npm run test:catalog && npm run changelog:validate", "lint:md": "npm run lint:md:links && npm run lint:md:style", "lint:md:links": "node scripts/lint-markdown-links.js", @@ -34,18 +34,20 @@ "test:binary": "jest --config jest.config.js --testMatch=\"**/tests/unit/binary-detection.test.{js,ts}\" --verbose", "test:patterns": "jest --config jest.config.js --testMatch=\"**/tests/**/*pattern*.test.{js,ts}\" --verbose", "test:lint": "npm run test && npm run lint:tests", - "watch:css": "npx @tailwindcss/cli -i ./src/renderer/styles.css -o ./src/renderer/output.css --watch", - "build:css": "npx @tailwindcss/cli -i ./src/renderer/styles.css -o ./src/renderer/output.css", + "prewatch:css": "node scripts/ensure-build-dirs.js", + "watch:css": "npx @tailwindcss/cli -i ./src/renderer/styles.css -o ./dist/renderer/output.css --watch", + "prebuild:css": "node scripts/ensure-build-dirs.js", + "build:css": "npx @tailwindcss/cli -i ./src/renderer/styles.css -o ./dist/renderer/output.css", "prepare": "husky install", "sonar": "node scripts/sonar-scan.js", "qa": "node scripts/index.js qa", - "preqa:screenshot": "npm run build:ts", + "preqa:screenshot": "npm run build:ts && npm run build:css && npm run build:webpack", "qa:screenshot": "node scripts/capture-ui-screenshot.js", "pree2e:playwright": "npm run build:ts && npm run build:css && npm run build:webpack", "e2e:playwright": "playwright test -c playwright.config.ts", "e2e:playwright:ci": "playwright test -c playwright.config.ts", "e2e:playwright:headed": "playwright test -c playwright.config.ts --headed", - "predocs:screenshots": "npm run build:ts && npm run build:webpack", + "predocs:screenshots": "npm run build:ts && npm run build:css && npm run build:webpack", "docs:screenshots": "node scripts/generate-doc-screenshots.js", "security": "node scripts/index.js security", "security:actions-freshness": "node scripts/audit-actions-freshness.js --report actions-freshness-report.md --json actions-freshness-report.json", @@ -89,6 +91,7 @@ "files": [ "src/**/*", "build/ts/**/*", + "dist/renderer/**/*", "package.json", "node_modules/**/*" ], diff --git a/scripts/capture-ui-screenshot.js b/scripts/capture-ui-screenshot.js index e9835d4..e444cee 100644 --- a/scripts/capture-ui-screenshot.js +++ b/scripts/capture-ui-screenshot.js @@ -6,7 +6,8 @@ const path = require('path'); const { chromium } = require('playwright'); const ROOT_DIR = path.join(__dirname, '..'); -const RENDERER_DIR = path.join(ROOT_DIR, 'src', 'renderer'); +const RENDERER_SOURCE_DIR = path.join(ROOT_DIR, 'src', 'renderer'); +const RENDERER_BUILD_DIR = path.join(ROOT_DIR, 'dist', 'renderer'); const DEFAULT_SCREENSHOT_DIR = path.join('dist', 'qa', 'screenshots'); const SCREENSHOT_DIR = resolveOutputDirectory(process.env.UI_SCREENSHOT_DIR); const PORT = Number(process.env.UI_SCREENSHOT_PORT || 4173); @@ -44,6 +45,16 @@ const MIME_TYPES = { '.svg': 'image/svg+xml', }; +const STATIC_FILE_ROUTES = new Map([ + ['/', path.join(RENDERER_SOURCE_DIR, 'index.html')], + ['/index.html', path.join(RENDERER_SOURCE_DIR, 'index.html')], + ['/icon.png', path.join(RENDERER_SOURCE_DIR, 'icon.png')], + ['/dist/renderer/output.css', path.join(RENDERER_BUILD_DIR, 'output.css')], + ['/dist/renderer/bundle.js', path.join(RENDERER_BUILD_DIR, 'bundle.js')], + ['/dist/renderer/bundle.js.map', path.join(RENDERER_BUILD_DIR, 'bundle.js.map')], + ['/dist/renderer/bundle.js.LICENSE.txt', path.join(RENDERER_BUILD_DIR, 'bundle.js.LICENSE.txt')], +]); + function ensureError(error) { if (error instanceof Error) { return error; @@ -106,16 +117,16 @@ async function runStep(stepName, action) { } function resolveFilePath(requestUrl) { - const urlPath = decodeURIComponent(requestUrl.split('?')[0]); - const relativePath = urlPath === '/' ? 'index.html' : urlPath.replace(/^\/+/, ''); - const absolutePath = path.resolve(RENDERER_DIR, relativePath); - const relativeToRoot = path.relative(RENDERER_DIR, absolutePath); + const rawPath = typeof requestUrl === 'string' ? requestUrl : '/'; + let urlPath; - if (relativeToRoot.startsWith('..') || path.isAbsolute(relativeToRoot)) { + try { + urlPath = decodeURIComponent(rawPath.split('?')[0]); + } catch { return null; } - return absolutePath; + return STATIC_FILE_ROUTES.get(urlPath) ?? null; } function createStaticServer() { @@ -516,6 +527,30 @@ async function setLocaleAndWait(page, locale) { ); } +async function setCheckboxState(page, selector, shouldBeChecked) { + const checkbox = page.locator(selector).first(); + await checkbox.waitFor({ state: 'visible', timeout: 10000 }); + + for (let attempt = 0; attempt < 3; attempt += 1) { + const currentState = await checkbox.isChecked(); + if (currentState === shouldBeChecked) { + return; + } + + if (shouldBeChecked) { + await checkbox.check(); + } else { + await checkbox.uncheck(); + } + + await page.waitForTimeout(75); + } + + throw new Error( + `Unable to set checkbox "${selector}" to ${shouldBeChecked ? 'checked' : 'unchecked'}` + ); +} + async function captureLocaleScreenshots(page) { await runStep('Wait for language selector', async () => { await page.waitForSelector(UI_SELECTORS.languageSelector, { timeout: 10000 }); @@ -597,8 +632,25 @@ async function captureAppStateScreenshots(page) { await runStep('Disable secret filtering in config tab', async () => { await page.click(UI_SELECTORS.configTab); await page.waitForSelector(UI_SELECTORS.secretScanningToggle, { timeout: 10000 }); - await page.uncheck(UI_SELECTORS.secretScanningToggle); - await page.uncheck(UI_SELECTORS.suspiciousFilesToggle); + await setCheckboxState(page, UI_SELECTORS.secretScanningToggle, false); + await setCheckboxState(page, UI_SELECTORS.suspiciousFilesToggle, false); + await page.waitForFunction( + ({ secretSelector, suspiciousSelector }) => { + const secretToggle = document.querySelector(secretSelector); + const suspiciousToggle = document.querySelector(suspiciousSelector); + + return ( + secretToggle instanceof HTMLInputElement && + suspiciousToggle instanceof HTMLInputElement && + !secretToggle.checked && + !suspiciousToggle.checked + ); + }, + { + secretSelector: UI_SELECTORS.secretScanningToggle, + suspiciousSelector: UI_SELECTORS.suspiciousFilesToggle, + } + ); await page.getByRole('button', { name: /save config|saved/i }).click(); await page.waitForFunction(() => { const configContent = localStorage.getItem('configContent') || ''; diff --git a/scripts/clean-dev-assets.js b/scripts/clean-dev-assets.js index 328c8b3..89c9957 100644 --- a/scripts/clean-dev-assets.js +++ b/scripts/clean-dev-assets.js @@ -9,10 +9,10 @@ const { rimraf } = require('rimraf'); // Asset paths relative to project root const assetPaths = [ - 'src/renderer/bundle.js', - 'src/renderer/bundle.js.map', - 'src/renderer/bundle.js.LICENSE.txt', - 'src/renderer/output.css', + 'dist/renderer/bundle.js', + 'dist/renderer/bundle.js.map', + 'dist/renderer/bundle.js.LICENSE.txt', + 'dist/renderer/output.css', ]; async function cleanDevAssets() { diff --git a/scripts/ensure-build-dirs.js b/scripts/ensure-build-dirs.js index 8b468a2..1058463 100644 --- a/scripts/ensure-build-dirs.js +++ b/scripts/ensure-build-dirs.js @@ -1,12 +1,13 @@ /** - * Script to ensure build directories exist for webpack + * Script to ensure build directories exist for renderer outputs and TypeScript builds. */ const fs = require('fs'); const path = require('path'); // Define build directory paths const buildDir = path.resolve(__dirname, '../build'); -const rendererDir = path.resolve(buildDir, 'renderer'); +const distDir = path.resolve(__dirname, '../dist'); +const rendererOutputDir = path.resolve(distDir, 'renderer'); // Create directories if they don't exist function ensureDirectoryExists(dirPath) { @@ -18,6 +19,7 @@ function ensureDirectoryExists(dirPath) { // Ensure all required directories exist ensureDirectoryExists(buildDir); -ensureDirectoryExists(rendererDir); +ensureDirectoryExists(distDir); +ensureDirectoryExists(rendererOutputDir); console.log('Build directories ready'); diff --git a/scripts/lib/dev.js b/scripts/lib/dev.js index 93e89db..1fe0a6b 100755 --- a/scripts/lib/dev.js +++ b/scripts/lib/dev.js @@ -29,25 +29,20 @@ async function start() { try { // Build CSS if it doesn't exist - const cssFile = path.join(utils.ROOT_DIR, 'src', 'renderer', 'output.css'); + const cssFile = path.join(utils.ROOT_DIR, 'dist', 'renderer', 'output.css'); if (!fs.existsSync(cssFile)) { log('CSS not found, building...', colors.yellow); try { - // Try direct command execution first - const { execSync } = require('child_process'); - log('Running tailwindcss directly...', colors.blue); - execSync('npx tailwindcss -i ./src/renderer/styles.css -o ./src/renderer/output.css', { - stdio: 'inherit', - cwd: utils.ROOT_DIR, - }); + log('Running build:css script...', colors.blue); + utils.runNpmScript('build:css'); } catch (err) { - log(`Error running tailwindcss: ${err.message}`, colors.red); + log(`Error running build:css: ${err.message}`, colors.red); throw err; } } // Check if webpack output exists - const webpackOutput = path.join(utils.ROOT_DIR, 'src', 'renderer', 'bundle.js'); + const webpackOutput = path.join(utils.ROOT_DIR, 'dist', 'renderer', 'bundle.js'); if (!fs.existsSync(webpackOutput)) { log('Webpack bundle not found, building...', colors.yellow); utils.runNpmScript('build:webpack'); diff --git a/scripts/lib/utils.js b/scripts/lib/utils.js index 3a385b8..6aed8bf 100755 --- a/scripts/lib/utils.js +++ b/scripts/lib/utils.js @@ -103,13 +103,7 @@ async function setupHooks() { // Clean functions async function cleanBuildArtifacts() { - const pathsToRemove = [ - path.join(ROOT_DIR, 'dist'), - path.join(ROOT_DIR, 'src', 'renderer', 'bundle.js'), - path.join(ROOT_DIR, 'src', 'renderer', 'bundle.js.map'), - path.join(ROOT_DIR, 'build', 'ts'), - path.join(ROOT_DIR, 'src', 'renderer', 'output.css'), - ]; + const pathsToRemove = [path.join(ROOT_DIR, 'dist'), path.join(ROOT_DIR, 'build', 'ts')]; console.log('Cleaning build artifacts...'); diff --git a/sonar-project.properties b/sonar-project.properties index 487b683..372653e 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -8,7 +8,7 @@ sonar.sources=src sonar.sourceEncoding=UTF-8 # Excluded directories and files (avoid noise from tests, fixtures, generated assets) -sonar.exclusions=node_modules/**,dist/**,coverage/**,tests/**,scripts/**,**/__tests__/**,**/*.test.js,**/*.test.jsx,**/*.test.ts,**/*.test.tsx,**/*.spec.js,**/*.spec.jsx,**/*.spec.ts,**/*.spec.tsx,src/renderer/bundle.js,src/renderer/bundle.js.map,src/renderer/bundle.js.LICENSE.txt,src/renderer/output.css,src/renderer/styles.css +sonar.exclusions=node_modules/**,dist/**,coverage/**,tests/**,scripts/**,**/__tests__/**,**/*.test.js,**/*.test.jsx,**/*.test.ts,**/*.test.tsx,**/*.spec.js,**/*.spec.jsx,**/*.spec.ts,**/*.spec.tsx,src/renderer/styles.css # Test directories (kept for coverage mapping, excluded from issue noise) sonar.tests=tests,src/__tests__ diff --git a/src/renderer/index.html b/src/renderer/index.html index 68722ef..63b639b 100755 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -4,7 +4,7 @@