diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..abe810c5 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +__fixtures__/ +dist/ diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml index 514ac27a..5468e6d0 100644 --- a/.github/workflows/auto-merge.yml +++ b/.github/workflows/auto-merge.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@v1.3.5 + uses: dependabot/fetch-metadata@v2.4.0 with: github-token: "${{ secrets.GITHUB_TOKEN }}" - name: Enable auto-merge for Dependabot PRs diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..39ce9123 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,35 @@ +name: Docs + +on: + workflow_dispatch: {} + push: + branches: + - main + +jobs: + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: 18 + cache: yarn + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Build docs + run: npm run docs:build + + # https://github.com/crazy-max/ghaction-github-pages + - name: Deploy to GitHub Pages + uses: crazy-max/ghaction-github-pages@v4 + with: + target_branch: gh-pages + build_dir: docs/.vitepress/dist + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 899842e8..896807e1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,14 +12,15 @@ jobs: CI: true steps: - uses: actions/checkout@master - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: - node-version: 14.x + node-version: 18 cache: yarn - name: Lint and test run: | yarn install --frozen-lockfile - yarn prepublishOnly + yarn build yarn lint yarn prettier --check '**/*' yarn test + yarn test:integration diff --git a/.gitignore b/.gitignore index 29efe008..9a0c8974 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ /.husky/ /.idea/ /dist/ -/node_modules/ +node_modules +/docs/.vitepress/dist +/docs/.vitepress/cache +yarn-error.log diff --git a/.npmignore b/.npmignore index 6e99e7ab..0e19c89a 100644 --- a/.npmignore +++ b/.npmignore @@ -1,4 +1,5 @@ .* bin/ +docs/ src/ jest.setup.ts diff --git a/.prettierignore b/.prettierignore index 1559e0d8..3f7e7daa 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,5 @@ .eslintcache +.eslintignore .gitignore .husky .npmignore @@ -6,3 +7,4 @@ LICENSE yarn.lock dist +__fixtures__ diff --git a/README.md b/README.md index a3660b36..f833decc 100644 --- a/README.md +++ b/README.md @@ -5,53 +5,15 @@ An `eslint` plugin for checking accessibility rules from within `.vue` files. -## Installation +## 📚 Documentation -If you're using `yarn`: +Please refer to the [official website](https://vue-a11y.github.io/eslint-plugin-vuejs-accessibility/). -```bash -yarn add --dev eslint-plugin-vuejs-accessibility -``` - -or if you're using `npm`: - -```bash -npm install --save-dev eslint-plugin-vuejs-accessibility -``` - -## Usage - -Add `vuejs-accessibility` to the plugins section of your `eslint` configuration. You can omit the `eslint-plugin-` prefix: - -```json -{ - "plugins": ["vuejs-accessibility"] -} -``` - -Then configure the rules you want to use under the rules section. - -```json -{ - "rules": { - "vuejs-accessibility/rule-name": "error" - } -} -``` - -You can also enable all the recommended rules at once. Add `plugin:vuejs-accessibility/recommended` in extends: - -```json -{ - "extends": ["plugin:vuejs-accessibility/recommended"] -} -``` - -## Development +## 💻 Development Ensure you have `node` and `yarn` installed on your system. Then run `yarn` in the root of the repository to install the dependencies. -### Adding a new rule +### 🔧 Adding a new rule To add a new rule, you need to take the following steps: @@ -60,18 +22,18 @@ To add a new rule, you need to take the following steps: - Add the corresponding test in `src/rules/__tests__`. - Add the corresponding documentation in `docs/rules`. -## Contributing +## 👨‍💻 Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/vue-a11y/eslint-plugin-vuejs-accessibility. -## License +## 📄 License The code is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). -## Credit +## 🏆 Credit The work for this plugin was largely based on previous work done on [eslint-plugin-vue-a11y](https://github.com/maranran/eslint-plugin-vue-a11y), as well as various other tools, including: -- [eslint-plugin-jsx-a11y](https://github.com/evcohen/eslint-plugin-jsx-a11y) +- [eslint-plugin-jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y) - [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) - [jsx-ast-utils](https://github.com/vuejs/eslint-plugin-vue) diff --git a/__tests__/integrations/__fixtures__/flat-config/.npmrc b/__tests__/integrations/__fixtures__/flat-config/.npmrc new file mode 100644 index 00000000..43c97e71 --- /dev/null +++ b/__tests__/integrations/__fixtures__/flat-config/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/__tests__/integrations/__fixtures__/flat-config/a.vue b/__tests__/integrations/__fixtures__/flat-config/a.vue new file mode 100644 index 00000000..6f0b8f75 --- /dev/null +++ b/__tests__/integrations/__fixtures__/flat-config/a.vue @@ -0,0 +1,11 @@ + + + diff --git a/__tests__/integrations/__fixtures__/flat-config/eslint.config.js b/__tests__/integrations/__fixtures__/flat-config/eslint.config.js new file mode 100644 index 00000000..5ae8ab92 --- /dev/null +++ b/__tests__/integrations/__fixtures__/flat-config/eslint.config.js @@ -0,0 +1,10 @@ +import plugin from "eslint-plugin-vuejs-accessibility"; + +export default [ + ...plugin.configs["flat/recommended"], + { + rules: { + "vuejs-accessibility/alt-text": "warn" + } + } +]; diff --git a/__tests__/integrations/__fixtures__/flat-config/package.json b/__tests__/integrations/__fixtures__/flat-config/package.json new file mode 100644 index 00000000..ce85990d --- /dev/null +++ b/__tests__/integrations/__fixtures__/flat-config/package.json @@ -0,0 +1,11 @@ +{ + "private": true, + "name": "integration-test-for-flat-config", + "type": "module", + "version": "1.0.0", + "description": "Integration test for flat config", + "dependencies": { + "eslint": "^9.0.0", + "eslint-plugin-vuejs-accessibility": "file:../../../.." + } +} diff --git a/__tests__/integrations/__fixtures__/legacy-config/.eslintrc.json b/__tests__/integrations/__fixtures__/legacy-config/.eslintrc.json new file mode 100644 index 00000000..b8124b09 --- /dev/null +++ b/__tests__/integrations/__fixtures__/legacy-config/.eslintrc.json @@ -0,0 +1,13 @@ +{ + "root": true, + "extends": ["plugin:vuejs-accessibility/recommended"], + "plugins": ["vuejs-accessibility"], + "parser": "vue-eslint-parser", + "parserOptions": { + "sourceType": "module", + "ecmaVersion": 2019 + }, + "rules": { + "vuejs-accessibility/alt-text": "warn" + } +} diff --git a/__tests__/integrations/__fixtures__/legacy-config/.npmrc b/__tests__/integrations/__fixtures__/legacy-config/.npmrc new file mode 100644 index 00000000..43c97e71 --- /dev/null +++ b/__tests__/integrations/__fixtures__/legacy-config/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/__tests__/integrations/__fixtures__/legacy-config/a.vue b/__tests__/integrations/__fixtures__/legacy-config/a.vue new file mode 100644 index 00000000..6f0b8f75 --- /dev/null +++ b/__tests__/integrations/__fixtures__/legacy-config/a.vue @@ -0,0 +1,11 @@ + + + diff --git a/__tests__/integrations/__fixtures__/legacy-config/package.json b/__tests__/integrations/__fixtures__/legacy-config/package.json new file mode 100644 index 00000000..34d02ee5 --- /dev/null +++ b/__tests__/integrations/__fixtures__/legacy-config/package.json @@ -0,0 +1,11 @@ +{ + "private": true, + "name": "integration-test-for-legacy-config", + "type": "module", + "version": "1.0.0", + "description": "Integration test for legacy config", + "dependencies": { + "eslint": "^8.57.0-0", + "eslint-plugin-vuejs-accessibility": "file:../../../.." + } +} diff --git a/__tests__/integrations/flat-config.spec.ts b/__tests__/integrations/flat-config.spec.ts new file mode 100644 index 00000000..e4998163 --- /dev/null +++ b/__tests__/integrations/flat-config.spec.ts @@ -0,0 +1,40 @@ +import cp from "child_process"; +import path from "path"; +import semver from "semver"; +import { readPackageJson } from "./helper"; + +const ESLINT = `.${path.sep}node_modules${path.sep}.bin${path.sep}eslint`; + +describe("Integration with flat config", () => { + let originalCwd: null | string = null; + const dirFixture = path.join(__dirname, "__fixtures__/flat-config"); + + beforeEach(() => { + originalCwd = process.cwd(); + process.chdir(dirFixture); + cp.execSync("npm i -f", { stdio: "inherit" }); + }); + afterEach(() => { + originalCwd && process.chdir(originalCwd); + }); + + it("should work with config", () => { + expect.assertions(2); + + const eslintPackageJson = readPackageJson( + path.resolve(dirFixture, "node_modules/eslint") + ); + + if (!semver.satisfies(process.version, eslintPackageJson.engines.node)) { + return; + } + + const result = JSON.parse( + cp.execSync(`${ESLINT} a.vue --max-warnings 1 --format=json`, { + encoding: "utf-8" + }) + ); + expect(result.length).toBe(1); + expect(result[0].messages[0].messageId).toBe("imgMissingAlt"); + }); +}); diff --git a/__tests__/integrations/helper.ts b/__tests__/integrations/helper.ts new file mode 100644 index 00000000..d69995d1 --- /dev/null +++ b/__tests__/integrations/helper.ts @@ -0,0 +1,8 @@ +import fs from "fs"; +import path from "path"; + +export function readPackageJson(base: string) { + return JSON.parse( + fs.readFileSync(path.resolve(base, "package.json"), "utf-8") + ); +} diff --git a/__tests__/integrations/legacy-config.spec.ts b/__tests__/integrations/legacy-config.spec.ts new file mode 100644 index 00000000..a7570d0d --- /dev/null +++ b/__tests__/integrations/legacy-config.spec.ts @@ -0,0 +1,40 @@ +import cp from "child_process"; +import path from "path"; +import semver from "semver"; +import { readPackageJson } from "./helper"; + +const ESLINT = `.${path.sep}node_modules${path.sep}.bin${path.sep}eslint`; + +describe("Integration with legacy config", () => { + let originalCwd: null | string = null; + const dirFixture = path.join(__dirname, "__fixtures__/legacy-config"); + + beforeEach(() => { + originalCwd = process.cwd(); + process.chdir(dirFixture); + cp.execSync("npm i -f", { stdio: "inherit" }); + }); + afterEach(() => { + originalCwd && process.chdir(originalCwd); + }); + + it("should work with config", () => { + expect.assertions(2); + + const eslintPackageJson = readPackageJson( + path.resolve(dirFixture, "node_modules/eslint") + ); + + if (!semver.satisfies(process.version, eslintPackageJson.engines.node)) { + return; + } + + const result = JSON.parse( + cp.execSync(`${ESLINT} a.vue --max-warnings 1 --format=json`, { + encoding: "utf-8" + }) + ); + expect(result.length).toBe(1); + expect(result[0].messages[0].messageId).toBe("imgMissingAlt"); + }); +}); diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts new file mode 100644 index 00000000..fc8dd263 --- /dev/null +++ b/docs/.vitepress/config.mts @@ -0,0 +1,83 @@ +import { defineConfig } from "vitepress"; +import { rules } from "./rulesForSidebar"; +import { description, version } from "../../package.json"; + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + title: "eslint-plugin-vuejs-a11y", + base: "/eslint-plugin-vuejs-accessibility/", + description, + head: [ + [ + "link", + { + rel: "icon", + type: "image/png", + sizes: "32x32", + href: "https://vue-a11y.com/favicon/favicon-32x32.png" + } + ], + [ + "link", + { + rel: "icon", + type: "image/png", + sizes: "96x96", + href: "https://vue-a11y.com/favicon/favicon-96x96.png" + } + ], + [ + "link", + { + rel: "icon", + type: "image/png", + sizes: "16x16", + href: "https://vue-a11y.com/favicon/favicon-16x16.png" + } + ] + ], + themeConfig: { + // https://vitepress.dev/reference/default-theme-config + nav: [ + { + text: version, + items: [ + { + text: "Changelog", + link: "https://github.com/vue-a11y/eslint-plugin-vuejs-accessibility/blob/main/CHANGELOG.md" + } + ] + } + ], + + sidebar: [ + { + text: "Introduction", + items: [ + { text: "Getting Started", link: "/" }, + { text: "Rule Overview", link: "/rule-overview/index" } + ] + }, + { + text: "Rules", + items: rules + } + ], + + editLink: { + pattern: + "https://github.com/vue-a11y/eslint-plugin-vuejs-accessibility/edit/main/docs/:path" + }, + + socialLinks: [ + { + icon: "github", + link: "https://github.com/vue-a11y/eslint-plugin-vuejs-accessibility" + } + ], + + search: { + provider: "local" + } + } +}); diff --git a/docs/.vitepress/rulesForSidebar.ts b/docs/.vitepress/rulesForSidebar.ts new file mode 100644 index 00000000..ca78e3ef --- /dev/null +++ b/docs/.vitepress/rulesForSidebar.ts @@ -0,0 +1,34 @@ +import { join, parse } from "node:path"; +import { Dirent, readdirSync } from "node:fs"; + +export const rules = getRulesForSideBar(); + +function getRulesForSideBar() { + const rulesDirectory = join(__dirname, "../", "rules"); + return readdirSync(rulesDirectory, { withFileTypes: true }) + .filter(isFile) + .filter(isMarkdown) + .map(fileNameWithoutExtension) + .map(ruleToSidebarItem); +} + +function isFile(dirent: Dirent) { + return !dirent.isDirectory(); +} + +function isMarkdown(dirent: Dirent) { + return dirent.name.endsWith(".md"); +} + +function fileNameWithoutExtension(file: Dirent) { + const parsedFileName = parse(file.name); + const nameWithoutExtension = parsedFileName.name; + return nameWithoutExtension; +} + +function ruleToSidebarItem(ruleName: string) { + return { + text: ruleName, + link: `/rules/${ruleName}` + }; +} diff --git a/docs/aria-role.md b/docs/aria-role.md deleted file mode 100644 index 74d13de7..00000000 --- a/docs/aria-role.md +++ /dev/null @@ -1,43 +0,0 @@ -# aria-role - -Elements with ARIA roles must use a valid, non-abstract ARIA role. - -_References:_ - -1. [WAI-ARIA](https://www.w3.org/TR/wai-aria/#role_definitions) site. -2. [AX_ARIA_01](https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_aria_01) -3. [DPUB-ARIA roles](https://www.w3.org/TR/dpub-aria-1.0/) - -## Rule details - -This rule takes one optional object argument of type object: - -```json -{ - "rules": { - "vuejs-accessibility/aria-role": ["error", { "ignoreNonDOM": true }] - } -} -``` - -### Succeed - -```vue -
- -
- -
- - - -``` - -### Fail - -```vue -
- -
- -``` diff --git a/docs/iframe-has-title.md b/docs/iframe-has-title.md deleted file mode 100644 index 7140d544..00000000 --- a/docs/iframe-has-title.md +++ /dev/null @@ -1,25 +0,0 @@ -# iframe-has-title - -`