@govela/eslint-plugin 0.0.0-stage → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +36 -2
- package/index.d.ts +14 -0
- package/index.js +186 -0
- package/package.json +29 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 abullo
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @govela/eslint-plugin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
ESLint for Vela components. The processor lints the `<script>` and the
|
|
4
|
+
template expressions of a `.vela` file as one program, the virtual
|
|
5
|
+
document of `vela document`: a variable used only in the template is
|
|
6
|
+
used, an undefined name in `{expression}` is reported at its place, and
|
|
7
|
+
the names of `{#each}` and `{#await}` are declared. Messages about
|
|
8
|
+
generated text are dropped; fixes inside the component's own text apply.
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm i -D @govela/eslint-plugin eslint @eslint/js globals
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
// eslint.config.js
|
|
16
|
+
import js from '@eslint/js';
|
|
17
|
+
import globals from 'globals';
|
|
18
|
+
import vela from '@govela/eslint-plugin';
|
|
19
|
+
|
|
20
|
+
export default [
|
|
21
|
+
js.configs.recommended,
|
|
22
|
+
...vela.configs.recommended,
|
|
23
|
+
// The code of the components: the virtual files App.vela/0_App.vela.js.
|
|
24
|
+
{ files: ['**/*.vela/*.js'], languageOptions: { globals: globals.browser } },
|
|
25
|
+
];
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
In TypeScript, typescript-eslint parses the virtual file of a `lang="ts"`
|
|
29
|
+
component (`**/*.vela/*.ts`). Type-aware rules cannot run on components:
|
|
30
|
+
turn them off there with `tseslint.configs.disableTypeChecked`.
|
|
31
|
+
|
|
32
|
+
`configs.recommended` sets the processor for `**/*.vela` and declares
|
|
33
|
+
`$state`, `$derived`, `$effect`, `$bindable` and `$$dispatch`. The
|
|
34
|
+
template markup is not linted. The compiler binary is the one of
|
|
35
|
+
`@govela/compiler` (or `VELA_BINARY`).
|
|
36
|
+
|
|
37
|
+
License: MIT.
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Types of @govela/eslint-plugin.
|
|
2
|
+
import type { ESLint, Linter } from 'eslint';
|
|
3
|
+
|
|
4
|
+
/** The compiler intrinsics, as globals of the components' code. */
|
|
5
|
+
export const globals: Record<'$state' | '$derived' | '$effect' | '$bindable' | '$$dispatch', 'readonly'>;
|
|
6
|
+
|
|
7
|
+
/** The processor of .vela files. */
|
|
8
|
+
export const processor: Linter.Processor;
|
|
9
|
+
|
|
10
|
+
declare const plugin: ESLint.Plugin & {
|
|
11
|
+
configs: { recommended: Linter.Config[] };
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default plugin;
|
package/index.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// @govela/eslint-plugin: ESLint for the JavaScript and TypeScript of Vela
|
|
2
|
+
// components (task 067). The processor lints the virtual document of a
|
|
3
|
+
// component, which `vela document` writes: the <script> as written, then the
|
|
4
|
+
// template expressions in a function whose blocks declare the {#each} and
|
|
5
|
+
// {#await} names. A variable used only in the template is therefore used,
|
|
6
|
+
// and a mistake in a template expression is reported at its place in the
|
|
7
|
+
// .vela file. Messages about the generated text are dropped.
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { readFileSync } from 'node:fs';
|
|
10
|
+
import { binaryPath } from '@govela/compiler';
|
|
11
|
+
|
|
12
|
+
const manifest = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
|
|
13
|
+
|
|
14
|
+
/** The compiler intrinsics, as globals of the components' code. */
|
|
15
|
+
export const globals = {
|
|
16
|
+
$state: 'readonly',
|
|
17
|
+
$derived: 'readonly',
|
|
18
|
+
$effect: 'readonly',
|
|
19
|
+
$bindable: 'readonly',
|
|
20
|
+
$$dispatch: 'readonly',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// The documents of the files being linted, from preprocess to postprocess.
|
|
24
|
+
const documents = new Map();
|
|
25
|
+
|
|
26
|
+
let binary;
|
|
27
|
+
function velaBinary() {
|
|
28
|
+
binary ??= binaryPath();
|
|
29
|
+
return binary;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function lineStarts(text) {
|
|
33
|
+
const starts = [0];
|
|
34
|
+
for (let i = 0; i < text.length; i++) {
|
|
35
|
+
if (text[i] === '\n') starts.push(i + 1);
|
|
36
|
+
}
|
|
37
|
+
return starts;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function offsetOf(starts, line, column) {
|
|
41
|
+
const start = starts[Math.min(Math.max(line, 1), starts.length) - 1];
|
|
42
|
+
return start + Math.max(column - 1, 0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function positionOf(starts, offset) {
|
|
46
|
+
let lo = 0;
|
|
47
|
+
let hi = starts.length - 1;
|
|
48
|
+
while (lo < hi) {
|
|
49
|
+
const mid = (lo + hi + 1) >> 1;
|
|
50
|
+
if (starts[mid] <= offset) lo = mid;
|
|
51
|
+
else hi = mid - 1;
|
|
52
|
+
}
|
|
53
|
+
return { line: lo + 1, column: offset - starts[lo] + 1 };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// The copied segment holding the virtual offset: an offset inside one, or
|
|
57
|
+
// at its end (a missing semicolon after the last statement).
|
|
58
|
+
function segmentAt(segments, offset) {
|
|
59
|
+
let found = null;
|
|
60
|
+
for (const segment of segments) {
|
|
61
|
+
const [, virt, length] = segment;
|
|
62
|
+
if (virt <= offset && offset < virt + length) return segment;
|
|
63
|
+
if (offset === virt + length) found ??= segment;
|
|
64
|
+
}
|
|
65
|
+
return found;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function document(text, filename) {
|
|
69
|
+
const result = spawnSync(velaBinary(), ['document', '--path', filename, '--stdin'], {
|
|
70
|
+
input: text,
|
|
71
|
+
encoding: 'utf8',
|
|
72
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
73
|
+
windowsHide: true,
|
|
74
|
+
});
|
|
75
|
+
if (result.error) throw new Error(`@govela/eslint-plugin: cannot run ${binary}: ${result.error.message}`);
|
|
76
|
+
if (result.status !== 0) throw new Error(`@govela/eslint-plugin: vela document ${filename}: ${result.stderr.trim()}`);
|
|
77
|
+
return JSON.parse(result.stdout);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function preprocess(text, filename) {
|
|
81
|
+
const doc = document(text, filename);
|
|
82
|
+
documents.set(filename, {
|
|
83
|
+
source: text,
|
|
84
|
+
sourceStarts: lineStarts(text),
|
|
85
|
+
virtualStarts: lineStarts(doc.text),
|
|
86
|
+
segments: doc.segments,
|
|
87
|
+
});
|
|
88
|
+
const base = filename.split(/[\\/]/).pop();
|
|
89
|
+
return [{ text: doc.text, filename: `${base}.${doc.typescript ? 'ts' : 'js'}` }];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// mapRange maps a virtual range into the component; null when it does not
|
|
93
|
+
// start in copied text. The end is clipped to the segment of the start.
|
|
94
|
+
function mapRange(doc, start, end) {
|
|
95
|
+
const segment = segmentAt(doc.segments, start);
|
|
96
|
+
if (!segment) return null;
|
|
97
|
+
const [src, virt, length] = segment;
|
|
98
|
+
const clipped = Math.min(Math.max(end, start), virt + length);
|
|
99
|
+
return [src + start - virt, src + clipped - virt];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// mapFix maps a fix whose range lies in one segment; others are dropped.
|
|
103
|
+
function mapFix(doc, fix) {
|
|
104
|
+
if (!fix) return undefined;
|
|
105
|
+
const [start, end] = fix.range;
|
|
106
|
+
const segment = segmentAt(doc.segments, start);
|
|
107
|
+
if (!segment) return null;
|
|
108
|
+
const [src, virt, length] = segment;
|
|
109
|
+
if (end > virt + length) return null;
|
|
110
|
+
return { range: [src + start - virt, src + end - virt], text: fix.text };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function mapMessage(doc, message) {
|
|
114
|
+
if (!message.line) return message;
|
|
115
|
+
const start = offsetOf(doc.virtualStarts, message.line, message.column);
|
|
116
|
+
const end = message.endLine ? offsetOf(doc.virtualStarts, message.endLine, message.endColumn) : start;
|
|
117
|
+
const range = mapRange(doc, start, end);
|
|
118
|
+
if (!range) {
|
|
119
|
+
// A parse error stays visible, at the start of the file.
|
|
120
|
+
return message.fatal ? { ...message, line: 1, column: 1, endLine: undefined, endColumn: undefined } : null;
|
|
121
|
+
}
|
|
122
|
+
const from = positionOf(doc.sourceStarts, range[0]);
|
|
123
|
+
const mapped = { ...message, line: from.line, column: from.column };
|
|
124
|
+
if (message.endLine) {
|
|
125
|
+
const to = positionOf(doc.sourceStarts, range[1]);
|
|
126
|
+
mapped.endLine = to.line;
|
|
127
|
+
mapped.endColumn = to.column;
|
|
128
|
+
}
|
|
129
|
+
if ('fix' in message) {
|
|
130
|
+
const fix = mapFix(doc, message.fix);
|
|
131
|
+
if (fix) mapped.fix = fix;
|
|
132
|
+
else delete mapped.fix;
|
|
133
|
+
}
|
|
134
|
+
if (message.suggestions) {
|
|
135
|
+
mapped.suggestions = message.suggestions
|
|
136
|
+
.map((s) => ({ ...s, fix: mapFix(doc, s.fix) }))
|
|
137
|
+
.filter((s) => s.fix);
|
|
138
|
+
}
|
|
139
|
+
return mapped;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function postprocess(messages, filename) {
|
|
143
|
+
const doc = documents.get(filename);
|
|
144
|
+
documents.delete(filename);
|
|
145
|
+
if (!doc) return messages.flat();
|
|
146
|
+
return messages.flat().map((message) => mapMessage(doc, message)).filter(Boolean);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The processor: `processor: 'vela/vela'` for `**\/*.vela`. */
|
|
150
|
+
export const processor = {
|
|
151
|
+
meta: { name: 'vela', version: manifest.version },
|
|
152
|
+
preprocess,
|
|
153
|
+
postprocess,
|
|
154
|
+
supportsAutofix: true,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const plugin = {
|
|
158
|
+
meta: { name: manifest.name, version: manifest.version },
|
|
159
|
+
processors: { vela: processor },
|
|
160
|
+
configs: {},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The recommended flat config: the processor for .vela files and the
|
|
165
|
+
* intrinsics as globals of their code. Add your own rules after it, for
|
|
166
|
+
* `**\/*.vela/*.js` (and `**\/*.vela/*.ts` with typescript-eslint).
|
|
167
|
+
*/
|
|
168
|
+
plugin.configs.recommended = [
|
|
169
|
+
{
|
|
170
|
+
name: 'vela/processor',
|
|
171
|
+
files: ['**/*.vela'],
|
|
172
|
+
plugins: { vela: plugin },
|
|
173
|
+
processor: 'vela/vela',
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: 'vela/globals',
|
|
177
|
+
files: ['**/*.vela/*.js', '**/*.vela/*.ts'],
|
|
178
|
+
languageOptions: {
|
|
179
|
+
ecmaVersion: 'latest',
|
|
180
|
+
sourceType: 'module',
|
|
181
|
+
globals,
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
export default plugin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@govela/eslint-plugin",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "ESLint for Vela components: a processor for the script and the template expressions of .vela files. Manifest generated by the compiler repository; do not edit.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/elauven/vela.git",
|
|
9
|
+
"directory": "packages/eslint-plugin"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./index.d.ts",
|
|
15
|
+
"default": "./index.js"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"types": "index.d.ts",
|
|
20
|
+
"files": [
|
|
21
|
+
"index.js",
|
|
22
|
+
"index.d.ts",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@govela/compiler": "0.7.0"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"eslint": ">=9"
|
|
30
|
+
}
|
|
31
|
+
}
|