├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.config.ts ├── demo └── app.vue ├── eslint.config.js ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src └── index.ts ├── test └── index.test.ts └── tsconfig.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [antfu] 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Install pnpm 19 | uses: pnpm/action-setup@v2 20 | 21 | - name: Set node 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: lts/* 25 | 26 | - name: Setup 27 | run: npm i -g @antfu/ni 28 | 29 | - name: Install 30 | run: nci 31 | 32 | - name: Build 33 | run: nr build 34 | 35 | - name: Lint 36 | run: nr lint 37 | 38 | typecheck: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v3 42 | 43 | - name: Install pnpm 44 | uses: pnpm/action-setup@v2 45 | 46 | - name: Set node 47 | uses: actions/setup-node@v3 48 | with: 49 | node-version: lts/* 50 | 51 | - name: Setup 52 | run: npm i -g @antfu/ni 53 | 54 | - name: Install 55 | run: nci 56 | 57 | - name: Build 58 | run: nr build 59 | 60 | - name: Typecheck 61 | run: nr typecheck 62 | 63 | test: 64 | runs-on: ${{ matrix.os }} 65 | 66 | strategy: 67 | matrix: 68 | node: [lts/*] 69 | os: [ubuntu-latest, windows-latest, macos-latest] 70 | fail-fast: false 71 | 72 | steps: 73 | - uses: actions/checkout@v3 74 | 75 | - name: Install pnpm 76 | uses: pnpm/action-setup@v2 77 | 78 | - name: Set node ${{ matrix.node }} 79 | uses: actions/setup-node@v3 80 | with: 81 | node-version: ${{ matrix.node }} 82 | 83 | - name: Setup 84 | run: npm i -g @antfu/ni 85 | 86 | - name: Install 87 | run: nci 88 | 89 | - name: Build 90 | run: nr build 91 | 92 | - name: Test 93 | run: nr test 94 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v*' 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Install pnpm 20 | uses: pnpm/action-setup@v2 21 | 22 | - name: Set node 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: lts/* 26 | 27 | - run: npx changelogithub 28 | env: 29 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .DS_Store 3 | .idea 4 | *.log 5 | *.tgz 6 | coverage 7 | dist 8 | lib-cov 9 | logs 10 | node_modules 11 | temp 12 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Enable the ESlint flat config support 3 | "eslint.experimental.useFlatConfig": true, 4 | 5 | // Disable the default formatter, use eslint instead 6 | "prettier.enable": false, 7 | "editor.formatOnSave": false, 8 | 9 | // Auto fix 10 | "editor.codeActionsOnSave": { 11 | "source.fixAll": "explicit", 12 | "source.organizeImports": "never" 13 | }, 14 | 15 | // Silent the stylistic rules in you IDE, but still auto fix them 16 | "eslint.rules.customizations": [ 17 | { "rule": "style/*", "severity": "off" }, 18 | { "rule": "*-indent", "severity": "off" }, 19 | { "rule": "*-spacing", "severity": "off" }, 20 | { "rule": "*-spaces", "severity": "off" }, 21 | { "rule": "*-order", "severity": "off" }, 22 | { "rule": "*-dangle", "severity": "off" }, 23 | { "rule": "*-newline", "severity": "off" }, 24 | { "rule": "*quotes", "severity": "off" }, 25 | { "rule": "*semi", "severity": "off" } 26 | ], 27 | 28 | // Enable eslint for all supported languages 29 | "eslint.validate": [ 30 | "javascript", 31 | "javascriptreact", 32 | "typescript", 33 | "typescriptreact", 34 | "vue", 35 | "html", 36 | "markdown", 37 | "json", 38 | "jsonc", 39 | "yaml" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please refer to https://github.com/antfu/contribute 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Anthony Fu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eslint-processor-vue-blocks 2 | 3 | [![npm version][npm-version-src]][npm-version-href] 4 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 5 | [![bundle][bundle-src]][bundle-href] 6 | [![JSDocs][jsdocs-src]][jsdocs-href] 7 | [![License][license-src]][license-href] 8 | 9 | Create virtual files in ESLint for each Vue SFC block, so that you can lint them individually. 10 | 11 | ## Install 12 | 13 | ```bash 14 | npm i -D eslint-processor-vue-blocks eslint-merge-processors 15 | ``` 16 | 17 | ## Usage 18 | 19 | In ESLint flat config: 20 | 21 | ```js 22 | // eslint.config.js 23 | import { mergeProcessors } from 'eslint-merge-processors' 24 | import pluginVue from 'eslint-plugin-vue' 25 | import processorVueBlocks from 'eslint-processor-vue-blocks' 26 | 27 | export default [ 28 | { 29 | files: ['*/*.vue'], 30 | plugins: { 31 | vue: pluginVue, 32 | }, 33 | // `eslint-plugin-vue` will set a default processor for `.vue` files 34 | // we use `eslint-merge-processors` to extend it 35 | processor: mergeProcessors([ 36 | pluginVue.processors['.vue'], 37 | processorVueBlocks({ 38 | blocks: { 39 | styles: true, 40 | customBlocks: true, 41 | // Usually it's not recommended to lint 8 | 9 | 14 | 15 | 20 | 21 | 27 | 28 | 29 | en: 30 | hello: Hello World 31 | 32 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import antfu from '@antfu/eslint-config' 3 | import { mergeProcessors } from 'eslint-merge-processors' 4 | import pluginVue from 'eslint-plugin-vue' 5 | import processorVueBlocks from 'eslint-processor-vue-blocks' 6 | 7 | export default antfu( 8 | { 9 | vue: true, 10 | formatters: { 11 | css: true, 12 | }, 13 | }, 14 | { 15 | files: ['**/*.vue'], 16 | processor: mergeProcessors([ 17 | pluginVue.processors['.vue'], 18 | processorVueBlocks({ 19 | blocks: { 20 | styles: true, 21 | }, 22 | }), 23 | ]), 24 | }, 25 | ) 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-processor-vue-blocks", 3 | "type": "module", 4 | "version": "2.0.0", 5 | "packageManager": "pnpm@10.2.1", 6 | "description": "Create virtual files in ESLint for each Vue SFC block, so that you can lint them individually.", 7 | "author": "Anthony Fu ", 8 | "license": "MIT", 9 | "funding": "https://github.com/sponsors/antfu", 10 | "homepage": "https://github.com/antfu/eslint-processor-vue-blocks#readme", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/antfu/eslint-processor-vue-blocks.git" 14 | }, 15 | "bugs": "https://github.com/antfu/eslint-processor-vue-blocks/issues", 16 | "keywords": [ 17 | "eslint", 18 | "eslint-processor", 19 | "vue" 20 | ], 21 | "sideEffects": false, 22 | "exports": { 23 | ".": "./dist/index.mjs" 24 | }, 25 | "main": "./dist/index.mjs", 26 | "module": "./dist/index.mjs", 27 | "types": "./dist/index.d.mts", 28 | "typesVersions": { 29 | "*": { 30 | "*": [ 31 | "./dist/*", 32 | "./dist/index.d.mts" 33 | ] 34 | } 35 | }, 36 | "files": [ 37 | "dist" 38 | ], 39 | "scripts": { 40 | "build": "unbuild", 41 | "stub": "unbuild --stub", 42 | "lint": "eslint .", 43 | "prepublishOnly": "nr build", 44 | "release": "bumpp && npm publish", 45 | "start": "tsx src/index.ts", 46 | "test": "vitest", 47 | "typecheck": "tsc --noEmit", 48 | "prepare": "simple-git-hooks" 49 | }, 50 | "peerDependencies": { 51 | "@vue/compiler-sfc": "^3.3.0", 52 | "eslint": ">=9.0.0" 53 | }, 54 | "devDependencies": { 55 | "@antfu/eslint-config": "^4.1.1", 56 | "@antfu/ni": "^23.3.1", 57 | "@antfu/utils": "^8.1.0", 58 | "@types/eslint": "^9.6.1", 59 | "@types/node": "^22.13.1", 60 | "@vue/compiler-sfc": "^3.5.13", 61 | "bumpp": "^10.0.2", 62 | "eslint": "^9.20.0", 63 | "eslint-merge-processors": "^2.0.0", 64 | "eslint-plugin-format": "^1.0.1", 65 | "eslint-plugin-vue": "^9.32.0", 66 | "eslint-processor-vue-blocks": "workspace:*", 67 | "lint-staged": "^15.4.3", 68 | "pnpm": "^10.2.1", 69 | "rimraf": "^6.0.1", 70 | "simple-git-hooks": "^2.11.1", 71 | "tsx": "^4.19.2", 72 | "typescript": "^5.7.3", 73 | "unbuild": "^3.3.1", 74 | "vite": "^6.1.0", 75 | "vitest": "^3.0.5", 76 | "vue": "^3.5.13" 77 | }, 78 | "simple-git-hooks": { 79 | "pre-commit": "pnpm lint-staged" 80 | }, 81 | "lint-staged": { 82 | "*": "eslint --fix" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - playground 3 | - examples/* 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { SFCBlock } from '@vue/compiler-sfc' 2 | import type { Linter } from 'eslint' 3 | import { parse } from '@vue/compiler-sfc' 4 | 5 | interface Block extends Linter.ProcessorFile { 6 | wrapper: TextWrapper 7 | startOffset: number 8 | } 9 | 10 | class TextWrapper { 11 | lines: string[] 12 | linesLength: number[] 13 | 14 | constructor( 15 | public text: string, 16 | ) { 17 | this.lines = text.split('\n') 18 | this.lines.forEach((_, index) => { 19 | if (index !== this.lines.length - 1) 20 | this.lines[index] += '\n' 21 | }) 22 | this.linesLength = this.lines.map(line => line.length) 23 | } 24 | 25 | getLineColumn(index: number) { 26 | let line = 0 27 | while (index >= this.linesLength[line]) { 28 | index -= this.linesLength[line] 29 | line++ 30 | } 31 | return { 32 | line: line + 1, 33 | column: index, 34 | } 35 | } 36 | 37 | getIndex(line: number, column: number) { 38 | return this.linesLength.slice(0, line - 1).reduce((a, b) => a + b, 0) + column 39 | } 40 | } 41 | 42 | const cache = new Map() 43 | 44 | export interface Options { 45 | blocks?: { 46 | /** 47 | * Create virtual files for each `