diff --git a/.github/workflows/frontend-quality.yml b/.github/workflows/frontend-quality.yml new file mode 100644 index 0000000..3597dee --- /dev/null +++ b/.github/workflows/frontend-quality.yml @@ -0,0 +1,49 @@ +name: Frontend Quality + +on: + pull_request: + paths: + - 'frontend/**' + - '.github/workflows/frontend-quality.yml' + push: + branches: + - main + - master + paths: + - 'frontend/**' + - '.github/workflows/frontend-quality.yml' + +permissions: + contents: read + +jobs: + frontend-quality: + runs-on: ubuntu-26.04 + defaults: + run: + working-directory: frontend + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: TypeScript check + run: npm run typecheck + + - name: Run tests + run: npm test diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs index 934c3a1..6333778 100644 --- a/frontend/eslint.config.mjs +++ b/frontend/eslint.config.mjs @@ -2,5 +2,61 @@ import withNuxt from './.nuxt/eslint.config.mjs' export default withNuxt( - // Your custom configs here + { + // Generated outputs are lint noise for app development. + ignores: ['.nuxt/**', '.output/**', 'dist/**', 'coverage/**', 'node_modules/**'], + }, + { + rules: { + // Keep developer flow smooth: warn for risky patterns, avoid hard errors. + 'no-debugger': 'warn', + 'no-alert': 'warn', + eqeqeq: ['warn', 'smart'], + curly: ['warn', 'multi-line'], + + // TypeScript projects should use TS-aware unused-vars checks. + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + + // Keep template attribute order predictable across the team. + 'vue/attributes-order': [ + 'warn', + { + order: [ + 'DEFINITION', + 'LIST_RENDERING', + 'CONDITIONALS', + 'RENDER_MODIFIERS', + 'GLOBAL', + ['UNIQUE', 'SLOT'], + 'TWO_WAY_BINDING', + 'OTHER_DIRECTIVES', + 'OTHER_ATTR', + 'EVENTS', + 'CONTENT', + ], + alphabetical: false, + }, + ], + 'vue/multi-word-component-names': 'off', + 'vue/require-default-prop': 'off', + }, + }, + { + files: ['**/*.ts', '**/*.tsx'], + rules: { + // This rule needs TS parser services, so limit it to TS files. + '@typescript-eslint/consistent-type-imports': [ + 'warn', + { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, + ], + }, + }, ) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ddbfb86..d9909b1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,9 @@ "pinia": "^4.0.2", "vue": "^3.5.40", "vue-router": "^5.2.0" + }, + "devDependencies": { + "vue-tsc": "^3.3.10" } }, "node_modules/@antfu/install-pkg": { @@ -3834,6 +3837,35 @@ "vue": "^3.0.0" } }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, "node_modules/@vue-macros/common": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.4.tgz", @@ -4025,6 +4057,22 @@ "integrity": "sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==", "license": "MIT" }, + "node_modules/@vue/language-core": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.10.tgz", + "integrity": "sha512-CR7ByBbgPHqhxrioKPOcZBqttaozzLNwtkCzXQ+uF8gLPHnUe03srPnGpdtHD3zp+bq5iyVkZ1WNx7W564RPwg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.2.1", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.4" + } + }, "node_modules/@vue/reactivity": { "version": "3.5.40", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.40.tgz", @@ -4149,6 +4197,13 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/alien-signals": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz", + "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==", + "devOptional": true, + "license": "MIT" + }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -8696,6 +8751,13 @@ "node": ">= 0.8" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "devOptional": true, + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -11492,6 +11554,13 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "devOptional": true, + "license": "MIT" + }, "node_modules/vue": { "version": "3.5.40", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.40.tgz", @@ -11673,6 +11742,23 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/vue-tsc": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.10.tgz", + "integrity": "sha512-YaDVxcW+CGtaOt3pZahMG5jYPx0hsUTxyEoPOTSMebcGUXP9lIBabQ14vfKMORb2CqqK5CxsNo/d1d+4IQwiKg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.3.10" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 1869185..184d8bc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,7 +7,12 @@ "dev": "nuxt dev", "generate": "nuxt generate", "preview": "nuxt preview", - "postinstall": "nuxt prepare" + "postinstall": "nuxt prepare", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "npm run lint:fix", + "typecheck": "nuxt typecheck", + "test": "node --test" }, "dependencies": { "@nuxt/eslint": "^1.16.0", @@ -18,5 +23,8 @@ "pinia": "^4.0.2", "vue": "^3.5.40", "vue-router": "^5.2.0" + }, + "devDependencies": { + "vue-tsc": "^3.3.10" } } diff --git a/frontend/tests/smoke.test.mjs b/frontend/tests/smoke.test.mjs new file mode 100644 index 0000000..cf52764 --- /dev/null +++ b/frontend/tests/smoke.test.mjs @@ -0,0 +1,8 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { existsSync } from 'node:fs' + +test('frontend entry pages exist', () => { + assert.equal(existsSync('app/pages/index.vue'), true) + assert.equal(existsSync('app/pages/health.vue'), true) +})