diff --git a/CHANGELOG.md b/CHANGELOG.md index e3d1f9b..711b4ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ยท versioned wi secrets, audio, and image payloads are excluded from persistence. - **Bounded Durable Job Retention.** Completed background-job metadata is pruned oldest-first at a fixed limit so restart-safe status tracking cannot become an unbounded database growth path. +- **Frontend Quality Gate.** Added `npm run check:frontend` to validate every manifest module for syntax errors, + duplicate manifest entries, and top-level declaration collisions before bundling. - **STT Cancellation Safety.** Queued transcription cancels before consuming the single STT slot, and timed-out requests keep the device slot held until the underlying worker actually exits. - **Credential Boundary Review.** Provider credentials are now documented as server-only; only the explicitly diff --git a/TODO.md b/TODO.md index 7e1a68f..e2c7b08 100644 --- a/TODO.md +++ b/TODO.md @@ -104,7 +104,8 @@ the first block before investing in architecture or polish. - [ ] Replace broad globals with namespaces such as `window.TTVC.audiobook`. - [x] Generate the feature-module list for `loader.js` and `scripts/minify.mjs` from one shared manifest; larger responsibility splits remain. -- [ ] Add linting and duplicate-function checks. +- [x] Add a dependency-free frontend syntax and top-level declaration collision gate (`npm run check:frontend`); + a full style/lint policy remains a follow-up. - [ ] Extract Rehearser voice matching and voice design. - [ ] Extract Audiobook parsing and casting. - [ ] Split `style.css` by feature. diff --git a/VERSION b/VERSION index a6c2798..49e0a31 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.23.0 +1.23.1 diff --git a/package.json b/package.json index 6f022db..c8d3c1b 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "test:headed": "playwright test --headed", "test:ui": "playwright test --ui", "install:browsers": "playwright install chromium", - "minify": "node scripts/minify.mjs" + "minify": "node scripts/minify.mjs", + "check:frontend": "node scripts/check-frontend.mjs" }, "devDependencies": { "@playwright/test": "^1.48.0", diff --git a/scripts/check-frontend.mjs b/scripts/check-frontend.mjs new file mode 100644 index 0000000..8a39997 --- /dev/null +++ b/scripts/check-frontend.mjs @@ -0,0 +1,36 @@ +// Fast, dependency-free frontend gate. The application deliberately shares one +// global scope, so duplicate top-level declarations are a runtime collision. +// Nested callbacks are ignored; vendor/PDF bundles are outside the manifest. +import { execFileSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = join(fileURLToPath(new URL('.', import.meta.url)), '..'); +const modules = JSON.parse(readFileSync(join(root, 'static', 'module-manifest.json'), 'utf8')); +const seenModules = new Set(); +const declarations = new Map(); +const declarationPattern = /^(?:async\s+)?(?:function|class|const|let|var)\s+([A-Za-z_$][\w$]*)/gm; + +for (const name of modules) { + if (seenModules.has(name)) throw new Error(`duplicate module in manifest: ${name}`); + seenModules.add(name); + const file = join(root, 'static', 'js', name + '.js'); + try { + execFileSync(process.execPath, ['--check', file], { stdio: 'pipe' }); + } catch (error) { + throw new Error(`JavaScript syntax check failed for ${file}: ${error.stderr?.toString().trim() || error.message}`); + } + const source = readFileSync(file, 'utf8'); + for (const match of source.matchAll(declarationPattern)) { + const files = declarations.get(match[1]) || []; + files.push(file); + declarations.set(match[1], files); + } +} + +const collisions = [...declarations.entries()].filter(([, files]) => files.length > 1); +if (collisions.length) { + throw new Error('duplicate top-level declarations:\n' + collisions.map(([name, files]) => ` ${name}: ${files.join(', ')}`).join('\n')); +} +console.log(`Frontend gate passed: ${modules.length} modules, no syntax errors or top-level declaration collisions.`);