@sinemacula/coding-standards 1.9.2 → 1.11.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/README.md +40 -4
- package/js/biome.json +16 -1
- package/js/eslint/index.js +13 -8
- package/js/eslint/plugin.js +2 -0
- package/js/eslint/rules/align-doc-tags.js +125 -0
- package/js/eslint/rules/boolean-method-name.js +30 -21
- package/js/eslint/rules/lib.js +6 -2
- package/js/eslint/rules/max-methods-per-class.js +4 -4
- package/js/eslint/rules/no-base-error.js +13 -7
- package/js/eslint/rules/no-interface-prefix.js +9 -3
- package/js/eslint/rules/no-mutable-static.js +45 -23
- package/js/eslint/rules/require-readonly-public-property.js +6 -2
- package/js/eslint/rules/valid-enum-member-name.js +3 -3
- package/js/eslint/vue.js +46 -0
- package/package.json +19 -3
package/README.md
CHANGED
|
@@ -121,13 +121,16 @@ typescript-eslint tooling, and this package to your dev dependencies:
|
|
|
121
121
|
npm install --save-dev eslint typescript typescript-eslint eslint-plugin-jsdoc @sinemacula/coding-standards
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
-
The package exposes
|
|
124
|
+
The package exposes three flat-config entry points:
|
|
125
125
|
|
|
126
126
|
- `@sinemacula/coding-standards/js/eslint` - the base layer of syntax-only custom rules; needs no `tsconfig`, so it
|
|
127
127
|
stays cheap and runs anywhere Biome runs.
|
|
128
128
|
- `@sinemacula/coding-standards/js/eslint/type-checked` - the opt-in type-aware layer. It includes the base layer and
|
|
129
129
|
adds the cross-file / type-driven rules, so it needs a consumer `tsconfig`; use it in place of the base layer where
|
|
130
130
|
one exists.
|
|
131
|
+
- `@sinemacula/coding-standards/js/eslint/vue` - the opt-in Vue layer for single-file components. Unlike the
|
|
132
|
+
type-aware layer it carries no base rules of its own, so spread it *alongside* whichever layer the repository
|
|
133
|
+
already uses rather than in place of one.
|
|
131
134
|
|
|
132
135
|
Create an `eslint.config.js` (or `.qlty/configs/eslint.config.js` when wired through Qlty) that spreads the layer you
|
|
133
136
|
want. Without a `tsconfig`, use the base layer:
|
|
@@ -146,6 +149,25 @@ import typeChecked from '@sinemacula/coding-standards/js/eslint/type-checked';
|
|
|
146
149
|
export default [...typeChecked];
|
|
147
150
|
```
|
|
148
151
|
|
|
152
|
+
Vue repositories add the Vue toolchain and spread the Vue layer after the layer they already use:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
npm install --save-dev eslint-plugin-vue vue-eslint-parser eslint-plugin-check-file
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
import typeChecked from '@sinemacula/coding-standards/js/eslint/type-checked';
|
|
160
|
+
import vue from '@sinemacula/coding-standards/js/eslint/vue';
|
|
161
|
+
|
|
162
|
+
export default [...typeChecked, ...vue];
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The Vue layer registers the single-file-component parser (without it `.vue` files are not linted at all), resolves
|
|
166
|
+
`<script lang="ts">` blocks through the TypeScript parser, and holds component filenames to kebab-case. It also
|
|
167
|
+
carries the template layout rules, which is the one place ESLint takes on formatting: Biome does not understand
|
|
168
|
+
single-file components, so `.vue` markup would otherwise go unformatted entirely. Those rules are aligned to the
|
|
169
|
+
shared four-space indent.
|
|
170
|
+
|
|
149
171
|
When wiring ESLint through Qlty, the shared eslint plugin sandbox installs only `eslint`, `jest`, and `prettier` by
|
|
150
172
|
default, so the flat config's imports of this package and `typescript-eslint` fail to resolve. Widen the install
|
|
151
173
|
filter in your `.qlty/qlty.toml` so the sandbox carries them (this repository's `source.toml` exports the same
|
|
@@ -156,6 +178,17 @@ override, but source-exported plugin definitions do not reliably propagate, so m
|
|
|
156
178
|
package_filters = ["@sinemacula/coding-standards", "typescript-eslint", "@typescript-eslint", "eslint-plugin-jsdoc"]
|
|
157
179
|
```
|
|
158
180
|
|
|
181
|
+
Repositories enabling the Vue layer widen the same filter further, since its plugins have to resolve inside that
|
|
182
|
+
sandbox too:
|
|
183
|
+
|
|
184
|
+
```toml
|
|
185
|
+
[plugins.definitions.eslint]
|
|
186
|
+
package_filters = [
|
|
187
|
+
"@sinemacula/coding-standards", "typescript-eslint", "@typescript-eslint", "eslint-plugin-jsdoc",
|
|
188
|
+
"eslint-plugin-vue", "vue-eslint-parser", "eslint-plugin-check-file",
|
|
189
|
+
]
|
|
190
|
+
```
|
|
191
|
+
|
|
159
192
|
### Knip (JavaScript / TypeScript)
|
|
160
193
|
|
|
161
194
|
```json
|
|
@@ -187,7 +220,7 @@ tag = "<version>"
|
|
|
187
220
|
| `php/phpstan-base.neon` | PHPStan | Base config (org-wide ignored errors + settings) |
|
|
188
221
|
| `js/biome.json` | Biome | JavaScript / TypeScript formatter + linter rules |
|
|
189
222
|
| `js/knip.json` | Knip | Unused-export detection rules |
|
|
190
|
-
| `js/eslint/` | ESLint |
|
|
223
|
+
| `js/eslint/` | ESLint | Structural, type-aware + Vue rules; runs with Biome |
|
|
191
224
|
| `markdown/.markdownlint.json` | markdownlint | Markdown linting rules |
|
|
192
225
|
| `yaml/.yamllint.yaml` | yamllint | YAML linting rules |
|
|
193
226
|
| `shell/.shellcheckrc` | ShellCheck | Shell script linting rules |
|
|
@@ -249,14 +282,17 @@ type-checked layer.
|
|
|
249
282
|
| `@sinemacula/max-methods-per-class` | A single class may declare at most 20 methods; test code exempt. |
|
|
250
283
|
| `@sinemacula/no-base-error` | Throw a domain-specific `Error` subclass, never the base `Error`; test code exempt. |
|
|
251
284
|
| `@sinemacula/require-copyright` | Every file must carry a documentation comment with `@copyright` and `@author`. |
|
|
285
|
+
| `@sinemacula/align-doc-tags` | `@author` and `@copyright` values line up at a single column; autofixable. |
|
|
252
286
|
|
|
253
287
|
`boolean-method-name` takes `additionalPrefixes`, `additionalPredicates` and `additionalCommandVerbs` (string arrays)
|
|
254
288
|
to widen the accepted vocabulary from a consumer config. `max-methods-per-class` takes `max`, `no-base-error` takes
|
|
255
|
-
`allow`, and `require-copyright` takes `tags` to adjust their defaults.
|
|
289
|
+
`allow`, and `require-copyright` takes `tags` to adjust their defaults. `align-doc-tags` takes `tags` and `column`,
|
|
290
|
+
the column counting from the `@`, so the default of 14 gives `@author` six spaces and `@copyright` three.
|
|
256
291
|
|
|
257
292
|
The base layer also switches on a set of built-in rules: `@typescript-eslint/no-explicit-any`, `max-lines-per-function`
|
|
258
293
|
(50 lines, test code exempt) and `max-depth` (4), plus `eslint-plugin-jsdoc` rules that require a documentation comment
|
|
259
|
-
on every declared function, method and class
|
|
294
|
+
on every declared function, method and class, forbid types in `@param`/`@returns` (the tags themselves are welcome,
|
|
295
|
+
types belong in the signature) and keep a blank line above every documentation block, single-line blocks included.
|
|
260
296
|
The type-checked layer adds `@typescript-eslint/explicit-module-boundary-types` and
|
|
261
297
|
`@typescript-eslint/only-throw-error`.
|
|
262
298
|
|
package/js/biome.json
CHANGED
|
@@ -60,6 +60,12 @@
|
|
|
60
60
|
"useConst": "warn",
|
|
61
61
|
"useDefaultParameterLast": "warn",
|
|
62
62
|
"useExponentiationOperator": "warn",
|
|
63
|
+
"useFilenamingConvention": {
|
|
64
|
+
"level": "warn",
|
|
65
|
+
"options": {
|
|
66
|
+
"filenameCases": ["kebab-case"]
|
|
67
|
+
}
|
|
68
|
+
},
|
|
63
69
|
"useNamingConvention": {
|
|
64
70
|
"level": "warn",
|
|
65
71
|
"options": {
|
|
@@ -98,7 +104,16 @@
|
|
|
98
104
|
"enabled": true,
|
|
99
105
|
"actions": {
|
|
100
106
|
"source": {
|
|
101
|
-
"organizeImports":
|
|
107
|
+
"organizeImports": {
|
|
108
|
+
"level": "on",
|
|
109
|
+
"options": {
|
|
110
|
+
"groups": [
|
|
111
|
+
[":BUN:", ":NODE:", ":PACKAGE_WITH_PROTOCOL:", ":URL:", ":PACKAGE:"],
|
|
112
|
+
":BLANK_LINE:",
|
|
113
|
+
[":ALIAS:", ":PATH:"]
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
}
|
|
102
117
|
}
|
|
103
118
|
}
|
|
104
119
|
},
|
package/js/eslint/index.js
CHANGED
|
@@ -8,11 +8,11 @@ const TS_AND_JS_FILES = [...TS_FILES, '**/*.js', '**/*.jsx', '**/*.mjs', '**/*.c
|
|
|
8
8
|
/**
|
|
9
9
|
* Base flat config: the AST-only custom rules that need no type information.
|
|
10
10
|
*
|
|
11
|
-
* Requires no tsconfig, so it stays cheap. The typescript-eslint parser
|
|
12
|
-
* TypeScript syntax. The interface, readonly-property and enum rules
|
|
13
|
-
* TypeScript-only constructs; no-mutable-static also applies to plain
|
|
14
|
-
* (exported let/var, mutable static fields), so it runs across both.
|
|
15
|
-
* type-aware layer lives in ./type-checked.js.
|
|
11
|
+
* Requires no tsconfig, so it stays cheap. The typescript-eslint parser
|
|
12
|
+
* resolves TypeScript syntax. The interface, readonly-property and enum rules
|
|
13
|
+
* target TypeScript-only constructs; no-mutable-static also applies to plain
|
|
14
|
+
* JavaScript (exported let/var, mutable static fields), so it runs across both.
|
|
15
|
+
* The opt-in type-aware layer lives in ./type-checked.js.
|
|
16
16
|
*
|
|
17
17
|
* @author Ben Carey <bdmc@sinemacula.co.uk>
|
|
18
18
|
* @copyright 2026 Sine Macula Limited
|
|
@@ -49,13 +49,17 @@ export default [
|
|
|
49
49
|
'@sinemacula/max-methods-per-class': 'error',
|
|
50
50
|
'@sinemacula/no-base-error': 'error',
|
|
51
51
|
'@sinemacula/require-copyright': 'error',
|
|
52
|
+
'@sinemacula/align-doc-tags': 'error',
|
|
52
53
|
|
|
53
54
|
'max-lines-per-function': ['error', { max: 50, skipComments: true, skipBlankLines: true, IIFEs: true }],
|
|
54
55
|
'max-depth': ['error', 4],
|
|
55
56
|
|
|
56
|
-
// Every declared function, method, class and assigned arrow carries
|
|
57
|
-
// documentation comment describing intent
|
|
58
|
-
// never
|
|
57
|
+
// Every declared function, method, class and assigned arrow carries
|
|
58
|
+
// a documentation comment describing intent. Types live in the
|
|
59
|
+
// signature, so a tag never annotates one: the @param and @returns
|
|
60
|
+
// tags themselves are welcome, only their type forms are not. Each
|
|
61
|
+
// block stands off from the code above it, single-line blocks
|
|
62
|
+
// included.
|
|
59
63
|
'jsdoc/require-jsdoc': ['error', {
|
|
60
64
|
require: {
|
|
61
65
|
ClassDeclaration: true,
|
|
@@ -73,6 +77,7 @@ export default [
|
|
|
73
77
|
'jsdoc/no-types': 'error',
|
|
74
78
|
'jsdoc/require-param-description': 'error',
|
|
75
79
|
'jsdoc/require-returns-description': 'error',
|
|
80
|
+
'jsdoc/lines-before-block': ['error', { lines: 1, ignoreSingleLines: false }],
|
|
76
81
|
},
|
|
77
82
|
},
|
|
78
83
|
{
|
package/js/eslint/plugin.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import alignDocTags from './rules/align-doc-tags.js';
|
|
1
2
|
import booleanMethodName from './rules/boolean-method-name.js';
|
|
2
3
|
import maxMethodsPerClass from './rules/max-methods-per-class.js';
|
|
3
4
|
import noBaseError from './rules/no-base-error.js';
|
|
@@ -30,5 +31,6 @@ export default {
|
|
|
30
31
|
'max-methods-per-class': maxMethodsPerClass,
|
|
31
32
|
'no-base-error': noBaseError,
|
|
32
33
|
'require-copyright': requireCopyright,
|
|
34
|
+
'align-doc-tags': alignDocTags,
|
|
33
35
|
},
|
|
34
36
|
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { createRule } from './lib.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_TAGS = ['author', 'copyright'];
|
|
4
|
+
const DEFAULT_COLUMN = 14;
|
|
5
|
+
|
|
6
|
+
/** A documentation line opening a tag that carries a value. */
|
|
7
|
+
const TAG_LINE = /^(\s*\*\s*)@([A-Za-z][\w-]*)([^\S\n]+)(?=\S)/;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The spaces that place a tag's value at the target column, or null when the
|
|
11
|
+
* tag is too long to reach it.
|
|
12
|
+
*/
|
|
13
|
+
function padding(tag, column) {
|
|
14
|
+
const spaces = column - tag.length - 2;
|
|
15
|
+
|
|
16
|
+
return spaces > 0 ? ' '.repeat(spaces) : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Align the values of the documentation tags a file's header declares,
|
|
21
|
+
* `@author` and `@copyright` by default.
|
|
22
|
+
*
|
|
23
|
+
* The tags a file must carry read as a block, so their values line up at a
|
|
24
|
+
* single column rather than sitting at whatever offset each tag's own length
|
|
25
|
+
* happens to produce. The column counts from the `@`, which at the default of
|
|
26
|
+
* 14 gives `@author` six spaces and `@copyright` three.
|
|
27
|
+
*
|
|
28
|
+
* Only the run of whitespace between a listed tag and its value is considered,
|
|
29
|
+
* and only on the line the tag opens; a wrapped value's continuation lines, a
|
|
30
|
+
* tag with no value and every unlisted tag are left alone. A tag longer than
|
|
31
|
+
* the column can accommodate is skipped rather than reported, as no spacing
|
|
32
|
+
* would satisfy the requirement. Presence of the tags is a separate concern,
|
|
33
|
+
* enforced by `require-copyright`.
|
|
34
|
+
*
|
|
35
|
+
* @author Ben Carey <bdmc@sinemacula.co.uk>
|
|
36
|
+
* @copyright 2026 Sine Macula Limited
|
|
37
|
+
*/
|
|
38
|
+
export default createRule({
|
|
39
|
+
name: 'align-doc-tags',
|
|
40
|
+
meta: {
|
|
41
|
+
type: 'layout',
|
|
42
|
+
fixable: 'whitespace',
|
|
43
|
+
docs: {
|
|
44
|
+
description: 'Align the values of the documentation tags a file header declares.',
|
|
45
|
+
},
|
|
46
|
+
schema: [
|
|
47
|
+
{
|
|
48
|
+
type: 'object',
|
|
49
|
+
properties: {
|
|
50
|
+
tags: {
|
|
51
|
+
type: 'array',
|
|
52
|
+
items: { type: 'string' },
|
|
53
|
+
},
|
|
54
|
+
column: {
|
|
55
|
+
type: 'integer',
|
|
56
|
+
minimum: 1,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
additionalProperties: false,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
messages: {
|
|
63
|
+
misaligned: 'The @{{ tag }} value must start at column {{ column }}.',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
defaultOptions: [{ tags: DEFAULT_TAGS, column: DEFAULT_COLUMN }],
|
|
67
|
+
create(context, [options]) {
|
|
68
|
+
const { sourceCode } = context;
|
|
69
|
+
const tags = new Set((options.tags ?? DEFAULT_TAGS).map(tag => tag.toLowerCase()));
|
|
70
|
+
const column = options.column ?? DEFAULT_COLUMN;
|
|
71
|
+
|
|
72
|
+
/** Report and fix the spacing a single tag line carries. */
|
|
73
|
+
function inspect(line, start) {
|
|
74
|
+
const match = TAG_LINE.exec(line);
|
|
75
|
+
|
|
76
|
+
if (!match) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const [, prefix, tag, spacing] = match;
|
|
81
|
+
|
|
82
|
+
if (!tags.has(tag.toLowerCase())) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const desired = padding(tag, column);
|
|
87
|
+
|
|
88
|
+
if (desired === null || spacing === desired) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const from = start + prefix.length + 1 + tag.length;
|
|
93
|
+
const to = from + spacing.length;
|
|
94
|
+
|
|
95
|
+
context.report({
|
|
96
|
+
loc: {
|
|
97
|
+
start: sourceCode.getLocFromIndex(from),
|
|
98
|
+
end: sourceCode.getLocFromIndex(to),
|
|
99
|
+
},
|
|
100
|
+
messageId: 'misaligned',
|
|
101
|
+
data: { tag, column },
|
|
102
|
+
fix: fixer => fixer.replaceTextRange([from, to], desired),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
Program() {
|
|
108
|
+
for (const comment of sourceCode.getAllComments()) {
|
|
109
|
+
if (comment.type !== 'Block' || !comment.value.startsWith('*')) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// The comment's value begins after the opening `/*`, so an
|
|
114
|
+
// offset within it maps onto the source two characters in.
|
|
115
|
+
let offset = comment.range[0] + 2;
|
|
116
|
+
|
|
117
|
+
for (const line of comment.value.split('\n')) {
|
|
118
|
+
inspect(line, offset);
|
|
119
|
+
offset += line.length + 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
});
|
|
@@ -52,8 +52,9 @@ function isCommandVerb(name, verbs) {
|
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
54
|
* Whether a return type resolves to boolean, ignoring a nullable `?bool`-style
|
|
55
|
-
* null/undefined/void tail so an optional boolean still counts. Promise
|
|
56
|
-
* are unwrapped before this is called, so an awaited boolean counts
|
|
55
|
+
* null/undefined/void tail so an optional boolean still counts. Promise
|
|
56
|
+
* wrappers are unwrapped before this is called, so an awaited boolean counts
|
|
57
|
+
* too.
|
|
57
58
|
*/
|
|
58
59
|
function returnsBoolean(type) {
|
|
59
60
|
if (type.flags & (ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral)) {
|
|
@@ -71,7 +72,9 @@ function returnsBoolean(type) {
|
|
|
71
72
|
return false;
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Whether the node is a function expression carrying an inspectable signature.
|
|
77
|
+
*/
|
|
75
78
|
function isFunctionExpression(node) {
|
|
76
79
|
return node != null
|
|
77
80
|
&& (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression');
|
|
@@ -90,7 +93,9 @@ function keyName(keyNode) {
|
|
|
90
93
|
return null;
|
|
91
94
|
}
|
|
92
95
|
|
|
93
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* Peel `as`/`satisfies`/non-null wrappers off an expression to reach the value.
|
|
98
|
+
*/
|
|
94
99
|
function unwrapExpression(node) {
|
|
95
100
|
let current = node;
|
|
96
101
|
|
|
@@ -128,8 +133,8 @@ function hasImperativeTag(sourceCode, docHost, nameNode) {
|
|
|
128
133
|
|
|
129
134
|
/**
|
|
130
135
|
* Report the name when it neither reads as a predicate nor is exempt and the
|
|
131
|
-
* resolved (awaited) return type is boolean. Type-predicate guards are
|
|
132
|
-
* by structure and left alone.
|
|
136
|
+
* resolved (awaited) return type is boolean. Type-predicate guards are
|
|
137
|
+
* predicates by structure and left alone.
|
|
133
138
|
*/
|
|
134
139
|
function inspect(state, nameNode, name, fnNode, docHost) {
|
|
135
140
|
const { checker, services, context, sourceCode } = state;
|
|
@@ -195,8 +200,9 @@ function inspectMember(state, node, allowEmptyBody) {
|
|
|
195
200
|
}
|
|
196
201
|
|
|
197
202
|
/**
|
|
198
|
-
* The visitor: inspect each named function, method, signature and
|
|
199
|
-
* member for a boolean return that does not read as a
|
|
203
|
+
* The visitor: inspect each named function, method, signature and
|
|
204
|
+
* function-valued member for a boolean return that does not read as a
|
|
205
|
+
* predicate.
|
|
200
206
|
*/
|
|
201
207
|
function buildListeners(state) {
|
|
202
208
|
return {
|
|
@@ -253,17 +259,19 @@ function buildListeners(state) {
|
|
|
253
259
|
* Boolean method name rule.
|
|
254
260
|
*
|
|
255
261
|
* A function, method, arrow-bound class field, object method or interface/type
|
|
256
|
-
* signature returning boolean should read as a predicate. A name is accepted
|
|
257
|
-
* its first camelCase word is a copular or modal prefix (is, has, can,
|
|
258
|
-
* idiomatic predicate from ALLOWED_PREDICATES (e.g. successful), or a
|
|
259
|
-
* in `s` (third-person: permits, passes) or `ed` (past tense:
|
|
260
|
-
* expired). An imperative command verb (execute, persist,
|
|
261
|
-
* a result bool is exempt via COMMAND_VERBS. A member
|
|
262
|
+
* signature returning boolean should read as a predicate. A name is accepted
|
|
263
|
+
* when its first camelCase word is a copular or modal prefix (is, has, can,
|
|
264
|
+
* ...), an idiomatic predicate from ALLOWED_PREDICATES (e.g. successful), or a
|
|
265
|
+
* verb ending in `s` (third-person: permits, passes) or `ed` (past tense:
|
|
266
|
+
* succeeded, failed, expired). An imperative command verb (execute, persist,
|
|
267
|
+
* guard, ...) that returns a result bool is exempt via COMMAND_VERBS. A member
|
|
268
|
+
* may also opt out with an
|
|
262
269
|
* @imperative docblock tag. Accessors, the constructor, computed names, magic
|
|
263
|
-
* names and type-predicate guards (x is T) are exempt. The return type is
|
|
264
|
-
* from type information - inferred booleans and awaited
|
|
265
|
-
* so the rule degrades to a no-op when no type
|
|
266
|
-
* accepted vocabulary can be widened per consumer
|
|
270
|
+
* names and type-predicate guards (x is T) are exempt. The return type is
|
|
271
|
+
* resolved from type information - inferred booleans and awaited
|
|
272
|
+
* Promise<boolean> included - so the rule degrades to a no-op when no type
|
|
273
|
+
* information is available. The accepted vocabulary can be widened per consumer
|
|
274
|
+
* through the rule options.
|
|
267
275
|
*
|
|
268
276
|
* @author Ben Carey <bdmc@sinemacula.co.uk>
|
|
269
277
|
* @copyright 2026 Sine Macula Limited
|
|
@@ -293,13 +301,14 @@ export default createRule({
|
|
|
293
301
|
const services = ESLintUtils.getParserServices(context, true);
|
|
294
302
|
|
|
295
303
|
// Without a type-checker program the return type can't be resolved, so
|
|
296
|
-
// the rule cannot decide anything; degrade to a no-op rather than
|
|
304
|
+
// the rule cannot decide anything; degrade to a no-op rather than
|
|
305
|
+
// throw.
|
|
297
306
|
if (!services.program) {
|
|
298
307
|
return {};
|
|
299
308
|
}
|
|
300
309
|
|
|
301
|
-
// Merge consumer additions onto the defaults so a downstream ruleset
|
|
302
|
-
// widen the accepted vocabulary without losing the built-in words.
|
|
310
|
+
// Merge consumer additions onto the defaults so a downstream ruleset
|
|
311
|
+
// can widen the accepted vocabulary without losing the built-in words.
|
|
303
312
|
const state = {
|
|
304
313
|
context,
|
|
305
314
|
services,
|
package/js/eslint/rules/lib.js
CHANGED
|
@@ -12,7 +12,9 @@ export const createRule = ESLintUtils.RuleCreator(
|
|
|
12
12
|
name => `https://github.com/sinemacula/coding-standards#${name}`,
|
|
13
13
|
);
|
|
14
14
|
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Whether the file is a TypeScript declaration file (.d.ts, .d.mts, .d.cts).
|
|
17
|
+
*/
|
|
16
18
|
export function isDeclarationFile(filename) {
|
|
17
19
|
return /\.d\.[cm]?ts$/.test(filename);
|
|
18
20
|
}
|
|
@@ -78,7 +80,9 @@ export function superClassName(klass) {
|
|
|
78
80
|
return null;
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* Whether the class reads as a test class (by its own or its parent's name).
|
|
85
|
+
*/
|
|
82
86
|
export function isTestClass(klass) {
|
|
83
87
|
if (klass.id?.name?.endsWith('Test')) {
|
|
84
88
|
return true;
|
|
@@ -2,8 +2,8 @@ import { createRule, isTestClass, isTestPath } from './lib.js';
|
|
|
2
2
|
|
|
3
3
|
/** Whether a class member counts towards the method total. */
|
|
4
4
|
function isCountedMethod(member) {
|
|
5
|
-
// An overload signature shares its name with the implementation below it,
|
|
6
|
-
// carries no body and is not counted a second time.
|
|
5
|
+
// An overload signature shares its name with the implementation below it,
|
|
6
|
+
// so it carries no body and is not counted a second time.
|
|
7
7
|
return member.type === 'MethodDefinition'
|
|
8
8
|
&& member.value.type !== 'TSEmptyBodyFunctionExpression';
|
|
9
9
|
}
|
|
@@ -28,8 +28,8 @@ function countMethods(node) {
|
|
|
28
28
|
* Every method declared on the class body counts, including the constructor,
|
|
29
29
|
* static methods and get/set accessors; a method spread across overload
|
|
30
30
|
* signatures counts once, through its implementation. Methods on a nested class
|
|
31
|
-
* belong to that class, not the one enclosing it. Test code legitimately
|
|
32
|
-
* many methods on one fixture, so a test file or test class is exempt.
|
|
31
|
+
* belong to that class, not the one enclosing it. Test code legitimately
|
|
32
|
+
* gathers many methods on one fixture, so a test file or test class is exempt.
|
|
33
33
|
*
|
|
34
34
|
* @author Ben Carey <bdmc@sinemacula.co.uk>
|
|
35
35
|
* @copyright 2026 Sine Macula Limited
|
|
@@ -3,7 +3,10 @@ import { createRule, isTestPath } from './lib.js';
|
|
|
3
3
|
/** Global objects whose `Error` member resolves to the base `Error`. */
|
|
4
4
|
const GLOBAL_OBJECTS = new Set(['globalThis', 'window', 'global', 'self']);
|
|
5
5
|
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Strips the `as`, `satisfies` and non-null wrappers a throw argument may
|
|
8
|
+
* carry.
|
|
9
|
+
*/
|
|
7
10
|
function unwrapType(node) {
|
|
8
11
|
let current = node;
|
|
9
12
|
|
|
@@ -36,7 +39,10 @@ function isGlobalError(callee) {
|
|
|
36
39
|
&& isErrorProperty(callee.property);
|
|
37
40
|
}
|
|
38
41
|
|
|
39
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Whether a construction callee denotes the base `Error`, directly or through a
|
|
44
|
+
* global object.
|
|
45
|
+
*/
|
|
40
46
|
function isBaseError(callee) {
|
|
41
47
|
if (callee.type === 'Identifier') {
|
|
42
48
|
return callee.name === 'Error';
|
|
@@ -55,11 +61,11 @@ function isBaseError(callee) {
|
|
|
55
61
|
* The check is syntactic. It flags a throw whose argument constructs the base
|
|
56
62
|
* `Error`, whether named directly (`new Error()`) or reached through a global
|
|
57
63
|
* object (`new globalThis.Error()`, `new window.Error()`). A `throw ... as X`,
|
|
58
|
-
* `throw ...!` or `satisfies` annotation is unwrapped before the construction
|
|
59
|
-
* inspected, so the annotation cannot hide the throw. Subclasses
|
|
60
|
-
* (`new NotFoundError()`) and the specific built-ins (`new TypeError()`) read
|
|
61
|
-
* domain-specific and pass; a re-thrown variable (`throw err`), a qualified
|
|
62
|
-
* (`new foo.Error()`) and a base `Error` built for a non-throw use
|
|
64
|
+
* `throw ...!` or `satisfies` annotation is unwrapped before the construction
|
|
65
|
+
* is inspected, so the annotation cannot hide the throw. Subclasses
|
|
66
|
+
* (`new NotFoundError()`) and the specific built-ins (`new TypeError()`) read
|
|
67
|
+
* as domain-specific and pass; a re-thrown variable (`throw err`), a qualified
|
|
68
|
+
* name (`new foo.Error()`) and a base `Error` built for a non-throw use
|
|
63
69
|
* (`const e = new Error()`) fall outside the pattern. Only the base `Error` is
|
|
64
70
|
* ever a candidate, so listing `Error` in the `allow` option is the one way to
|
|
65
71
|
* permit it; any other name has nothing to match and stays inert.
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { createRule } from './lib.js';
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Disallowed "I" prefix: a capital I directly followed by another uppercase
|
|
5
|
+
* letter.
|
|
6
|
+
*/
|
|
4
7
|
const PREFIX_PATTERN = /^I[A-Z]/;
|
|
5
8
|
|
|
6
9
|
/** A global or string-named module block augments types we do not own. */
|
|
@@ -31,7 +34,9 @@ export default createRule({
|
|
|
31
34
|
create(context) {
|
|
32
35
|
const { sourceCode } = context;
|
|
33
36
|
|
|
34
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Report a declaration whose name carries the disallowed "I" prefix.
|
|
39
|
+
*/
|
|
35
40
|
const check = (node, kind) => {
|
|
36
41
|
const name = node.id.name;
|
|
37
42
|
|
|
@@ -39,7 +44,8 @@ export default createRule({
|
|
|
39
44
|
return;
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
// Augmenting an external module or the global scope cannot rename
|
|
47
|
+
// Augmenting an external module or the global scope cannot rename
|
|
48
|
+
// it.
|
|
43
49
|
if (sourceCode.getAncestors(node).some(isExternalAugmentation)) {
|
|
44
50
|
return;
|
|
45
51
|
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { ASTUtils } from '@typescript-eslint/utils';
|
|
2
2
|
import { createRule, isAmbient, isDeclarationFile, isTestClass, isTestPath, nearestClass } from './lib.js';
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Unwrap a rest element to its bound target, else return the node unchanged.
|
|
6
|
+
*/
|
|
5
7
|
function restTarget(node) {
|
|
6
8
|
return node.type === 'RestElement' ? node.argument : node;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* The destructuring children of a pattern that may bind further identifiers.
|
|
13
|
+
*/
|
|
10
14
|
function patternChildren(pattern) {
|
|
11
15
|
switch (pattern.type) {
|
|
12
16
|
case 'ArrayPattern':
|
|
@@ -22,7 +26,10 @@ function patternChildren(pattern) {
|
|
|
22
26
|
}
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Collect the bound identifiers of a declarator target, unwrapping
|
|
31
|
+
* destructuring.
|
|
32
|
+
*/
|
|
26
33
|
function boundIdentifiers(pattern, out) {
|
|
27
34
|
if (pattern.type === 'Identifier') {
|
|
28
35
|
out.push(pattern);
|
|
@@ -36,7 +43,8 @@ function boundIdentifiers(pattern, out) {
|
|
|
36
43
|
|
|
37
44
|
/**
|
|
38
45
|
* Whether a resolved binding is a mutable local `let`/`var` (not const, class,
|
|
39
|
-
* function, import or an ambient declaration), and so publishes live module
|
|
46
|
+
* function, import or an ambient declaration), and so publishes live module
|
|
47
|
+
* state.
|
|
40
48
|
*/
|
|
41
49
|
function bindsMutableVariable(variable, filename) {
|
|
42
50
|
if (isDeclarationFile(filename)) {
|
|
@@ -48,7 +56,9 @@ function bindsMutableVariable(variable, filename) {
|
|
|
48
56
|
);
|
|
49
57
|
}
|
|
50
58
|
|
|
51
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Matches @managed-static only at a docblock tag position, never inside prose.
|
|
61
|
+
*/
|
|
52
62
|
const MANAGED_TAG = /(?:^|[\s*])@managed-static(?![-\w])/i;
|
|
53
63
|
|
|
54
64
|
/**
|
|
@@ -62,8 +72,8 @@ function hasManagedTag(node, sourceCode) {
|
|
|
62
72
|
return true;
|
|
63
73
|
}
|
|
64
74
|
|
|
65
|
-
// A docblock tucked between the decorators and the declaration sits before
|
|
66
|
-
// first token after the last decorator, not before the whole member.
|
|
75
|
+
// A docblock tucked between the decorators and the declaration sits before
|
|
76
|
+
// the first token after the last decorator, not before the whole member.
|
|
67
77
|
if (node.decorators?.length) {
|
|
68
78
|
const afterDecorators = sourceCode.getTokenAfter(node.decorators.at(-1));
|
|
69
79
|
const inner = afterDecorators && sourceCode.getCommentsBefore(afterDecorators).at(-1);
|
|
@@ -74,7 +84,9 @@ function hasManagedTag(node, sourceCode) {
|
|
|
74
84
|
return false;
|
|
75
85
|
}
|
|
76
86
|
|
|
77
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* A readable name for a class member key, including private and computed forms.
|
|
89
|
+
*/
|
|
78
90
|
function describeKey(node, sourceCode) {
|
|
79
91
|
if (node.computed) {
|
|
80
92
|
return `[${sourceCode.getText(node.key)}]`;
|
|
@@ -91,7 +103,10 @@ function describeKey(node, sourceCode) {
|
|
|
91
103
|
return node.key.name;
|
|
92
104
|
}
|
|
93
105
|
|
|
94
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* Report each identifier bound by an inline `export let`/`export var`
|
|
108
|
+
* declaration.
|
|
109
|
+
*/
|
|
95
110
|
function reportInlineExports(node, context) {
|
|
96
111
|
const declaration = node.declaration;
|
|
97
112
|
|
|
@@ -116,7 +131,8 @@ function reportInlineExports(node, context) {
|
|
|
116
131
|
|
|
117
132
|
/** Report `export { x }` specifiers that publish a mutable local binding. */
|
|
118
133
|
function reportSpecifierExports(node, context) {
|
|
119
|
-
// A re-export carries no local binding; a type-only export carries no
|
|
134
|
+
// A re-export carries no local binding; a type-only export carries no
|
|
135
|
+
// runtime one.
|
|
120
136
|
if (node.source || node.exportKind === 'type') {
|
|
121
137
|
return;
|
|
122
138
|
}
|
|
@@ -137,18 +153,18 @@ function reportSpecifierExports(node, context) {
|
|
|
137
153
|
}
|
|
138
154
|
|
|
139
155
|
/**
|
|
140
|
-
* Forbids mutable module-level and class-level static state: exported
|
|
141
|
-
* bindings (declared inline or published through an
|
|
142
|
-
* and non-readonly `static` class fields, including
|
|
143
|
-
* auto-accessors. All are global mutable state; `const` and
|
|
144
|
-
* read-only configuration this allows.
|
|
156
|
+
* Forbids mutable module-level and class-level static state: exported
|
|
157
|
+
* `let`/`var` bindings (declared inline or published through an
|
|
158
|
+
* `export { ... }` specifier) and non-readonly `static` class fields, including
|
|
159
|
+
* `static accessor` auto-accessors. All are global mutable state; `const` and
|
|
160
|
+
* `readonly` express the read-only configuration this allows.
|
|
145
161
|
*
|
|
146
|
-
* The check is syntactic, not write-sensitive: a static is flagged whether or
|
|
147
|
-
* a reassignment is visible, since cross-file writes are out of a per-file
|
|
148
|
-
* reach. Deliberately mutated statics opt out with a `@managed-static`
|
|
149
|
-
* the field or its declaring class, and test classes are exempt.
|
|
150
|
-
* limited to exported bindings; an unexported module `let`
|
|
151
|
-
* alone.
|
|
162
|
+
* The check is syntactic, not write-sensitive: a static is flagged whether or
|
|
163
|
+
* not a reassignment is visible, since cross-file writes are out of a per-file
|
|
164
|
+
* rule's reach. Deliberately mutated statics opt out with a `@managed-static`
|
|
165
|
+
* doc tag on the field or its declaring class, and test classes are exempt.
|
|
166
|
+
* Module scope is limited to exported bindings; an unexported module `let`
|
|
167
|
+
* stays local and is left alone.
|
|
152
168
|
*
|
|
153
169
|
* @author Ben Carey <bdmc@sinemacula.co.uk>
|
|
154
170
|
* @copyright 2026 Sine Macula Limited
|
|
@@ -170,7 +186,10 @@ export default createRule({
|
|
|
170
186
|
create(context) {
|
|
171
187
|
const { sourceCode } = context;
|
|
172
188
|
|
|
173
|
-
/**
|
|
189
|
+
/**
|
|
190
|
+
* Whether a static member is exempt: test code or an opted-out
|
|
191
|
+
* declaration.
|
|
192
|
+
*/
|
|
174
193
|
const isStaticExempt = node => {
|
|
175
194
|
if (isTestPath(context.filename)) {
|
|
176
195
|
return true;
|
|
@@ -185,7 +204,10 @@ export default createRule({
|
|
|
185
204
|
return hasManagedTag(node, sourceCode) || (klass !== null && hasManagedTag(klass, sourceCode));
|
|
186
205
|
};
|
|
187
206
|
|
|
188
|
-
/**
|
|
207
|
+
/**
|
|
208
|
+
* Flag a non-readonly static field or auto-accessor as mutable static
|
|
209
|
+
* state.
|
|
210
|
+
*/
|
|
189
211
|
const inspectStatic = node => {
|
|
190
212
|
if (!node.static || node.readonly || isAmbient(node, context.filename) || isStaticExempt(node)) {
|
|
191
213
|
return;
|
|
@@ -82,7 +82,8 @@ export default createRule({
|
|
|
82
82
|
});
|
|
83
83
|
},
|
|
84
84
|
'AccessorProperty, TSAbstractAccessorProperty'(node) {
|
|
85
|
-
// An auto-accessor cannot be readonly, so a public one is
|
|
85
|
+
// An auto-accessor cannot be readonly, so a public one is
|
|
86
|
+
// always mutable.
|
|
86
87
|
if (isOutOfScope(node) || isExempt(node)) {
|
|
87
88
|
return;
|
|
88
89
|
}
|
|
@@ -115,7 +116,10 @@ function isNonPublic(node) {
|
|
|
115
116
|
|| node.accessibility === 'protected';
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* Whether a class property is outside the public-mutable scope: static,
|
|
121
|
+
* ambient, or non-public.
|
|
122
|
+
*/
|
|
119
123
|
function isOutOfScope(node) {
|
|
120
124
|
return node.static || node.declare || isNonPublic(node);
|
|
121
125
|
}
|
|
@@ -32,9 +32,9 @@ export default createRule({
|
|
|
32
32
|
return {
|
|
33
33
|
TSEnumMember(node) {
|
|
34
34
|
const id = node.id;
|
|
35
|
-
// A member name is either a bare identifier or a string
|
|
36
|
-
// a string literal is checked by its resolved value,
|
|
37
|
-
// source text.
|
|
35
|
+
// A member name is either a bare identifier or a string
|
|
36
|
+
// literal; a string literal is checked by its resolved value,
|
|
37
|
+
// not the raw source text.
|
|
38
38
|
const name = id.type === 'Identifier' ? id.name : String(id.value);
|
|
39
39
|
|
|
40
40
|
if (!NAME_PATTERN.test(name)) {
|
package/js/eslint/vue.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import checkFile from 'eslint-plugin-check-file';
|
|
2
|
+
import pluginVue from 'eslint-plugin-vue';
|
|
3
|
+
import tseslint from 'typescript-eslint';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Opt-in Vue layer: the single-file-component rules. Registers the SFC parser
|
|
7
|
+
* so `.vue` files are linted at all, resolves `<script lang="ts">` blocks
|
|
8
|
+
* through the TypeScript parser, and holds component filenames to the same
|
|
9
|
+
* kebab-case convention the shared formatter enforces on plain sources.
|
|
10
|
+
*
|
|
11
|
+
* This layer also carries the template layout rules, which the shared
|
|
12
|
+
* formatter cannot: it does not understand single-file components, so `.vue`
|
|
13
|
+
* markup would otherwise go unformatted entirely.
|
|
14
|
+
*
|
|
15
|
+
* Additive, and deliberately unlike the type-aware layer: it carries no base
|
|
16
|
+
* rules of its own, so spread it after whichever layer a repo already uses
|
|
17
|
+
* rather than in place of one. Vue is orthogonal to type-awareness, and a
|
|
18
|
+
* repo enabling both would otherwise apply the base rules twice.
|
|
19
|
+
*
|
|
20
|
+
* @author Ben Carey <bdmc@sinemacula.co.uk>
|
|
21
|
+
* @copyright 2026 Sine Macula Limited
|
|
22
|
+
*/
|
|
23
|
+
export default [
|
|
24
|
+
...pluginVue.configs['flat/recommended-error'],
|
|
25
|
+
{
|
|
26
|
+
files: ['**/*.vue'],
|
|
27
|
+
plugins: {
|
|
28
|
+
'check-file': checkFile,
|
|
29
|
+
},
|
|
30
|
+
languageOptions: {
|
|
31
|
+
parserOptions: {
|
|
32
|
+
parser: tseslint.parser,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
rules: {
|
|
36
|
+
// The template rules indent by two; the shared formatter is four.
|
|
37
|
+
'vue/html-indent': ['error', 4],
|
|
38
|
+
|
|
39
|
+
'check-file/filename-naming-convention': ['error', {
|
|
40
|
+
'**/*.vue': 'KEBAB_CASE',
|
|
41
|
+
}, {
|
|
42
|
+
ignoreMiddleExtensions: true,
|
|
43
|
+
}],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sinemacula/coding-standards",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "Centralized coding standards, static analysis configurations, and code quality tooling for all Sine Macula repositories.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Ben Carey <bdmc@sinemacula.co.uk>",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"exports": {
|
|
36
36
|
"./js/eslint": "./js/eslint/index.js",
|
|
37
37
|
"./js/eslint/type-checked": "./js/eslint/type-checked.js",
|
|
38
|
+
"./js/eslint/vue": "./js/eslint/vue.js",
|
|
38
39
|
"./*": "./*"
|
|
39
40
|
},
|
|
40
41
|
"scripts": {
|
|
@@ -45,29 +46,44 @@
|
|
|
45
46
|
"@typescript-eslint/rule-tester": "^8.0.0",
|
|
46
47
|
"@typescript-eslint/utils": "^8.0.0",
|
|
47
48
|
"eslint": "^9.0.0",
|
|
49
|
+
"eslint-plugin-check-file": "^3.0.0",
|
|
48
50
|
"eslint-plugin-jsdoc": "^63.0.13",
|
|
51
|
+
"eslint-plugin-vue": "^10.0.0",
|
|
49
52
|
"typescript": "^5.0.0",
|
|
50
53
|
"typescript-eslint": "^8.0.0",
|
|
51
|
-
"vitest": "^3.0.0"
|
|
54
|
+
"vitest": "^3.0.0",
|
|
55
|
+
"vue-eslint-parser": "^10.0.0"
|
|
52
56
|
},
|
|
53
57
|
"peerDependencies": {
|
|
54
58
|
"eslint": ">=9",
|
|
59
|
+
"eslint-plugin-check-file": ">=3",
|
|
55
60
|
"eslint-plugin-jsdoc": ">=48",
|
|
61
|
+
"eslint-plugin-vue": ">=10",
|
|
56
62
|
"typescript": ">=4.8.4",
|
|
57
|
-
"typescript-eslint": "^8"
|
|
63
|
+
"typescript-eslint": "^8",
|
|
64
|
+
"vue-eslint-parser": ">=10"
|
|
58
65
|
},
|
|
59
66
|
"peerDependenciesMeta": {
|
|
60
67
|
"eslint": {
|
|
61
68
|
"optional": true
|
|
62
69
|
},
|
|
70
|
+
"eslint-plugin-check-file": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
63
73
|
"eslint-plugin-jsdoc": {
|
|
64
74
|
"optional": true
|
|
65
75
|
},
|
|
76
|
+
"eslint-plugin-vue": {
|
|
77
|
+
"optional": true
|
|
78
|
+
},
|
|
66
79
|
"typescript": {
|
|
67
80
|
"optional": true
|
|
68
81
|
},
|
|
69
82
|
"typescript-eslint": {
|
|
70
83
|
"optional": true
|
|
84
|
+
},
|
|
85
|
+
"vue-eslint-parser": {
|
|
86
|
+
"optional": true
|
|
71
87
|
}
|
|
72
88
|
},
|
|
73
89
|
"publishConfig": {
|