@jskit-ai/config-eslint 0.1.241 → 0.1.243
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 +135 -0
- package/base.js +5 -1
- package/clientFeatures.js +87 -0
- package/formatter.js +60 -0
- package/index.js +4 -0
- package/package.json +8 -1
- package/plugin/factoryResults.js +162 -0
- package/plugin/index.js +64 -0
- package/plugin/packageSources.js +133 -0
- package/plugin/rules/actionCreator.js +55 -0
- package/plugin/rules/actionExecuteShape.js +75 -0
- package/plugin/rules/allowCommentForm.js +145 -0
- package/plugin/rules/appErrorCode.js +169 -0
- package/plugin/rules/collaboratorProps.js +130 -0
- package/plugin/rules/fixedResultShape.js +96 -0
- package/plugin/rules/frozenFactoryResult.js +44 -0
- package/plugin/rules/jsdocDescriptionInside.js +45 -0
- package/plugin/rules/namedRepeatedType.js +178 -0
- package/plugin/rules/noDualShape.js +61 -0
- package/plugin/rules/noDuplicateHelper.js +146 -0
- package/plugin/rules/noEnvDefault.js +33 -0
- package/plugin/rules/noFactorySpread.js +99 -0
- package/plugin/rules/noFrozenVueState.js +48 -0
- package/plugin/rules/noIdempotency.js +26 -0
- package/plugin/rules/noModuleRegistry.js +61 -0
- package/plugin/rules/noModuleState.js +50 -0
- package/plugin/rules/noOptionalAfterDefault.js +91 -0
- package/plugin/rules/noPatchedError.js +50 -0
- package/plugin/rules/noSlotSetter.js +40 -0
- package/plugin/rules/noTestOnlyExport.js +38 -0
- package/plugin/rules/oneLineAction.js +98 -0
- package/plugin/rules/onePropertyPerLine.js +86 -0
- package/plugin/rules/releasedListeners.js +132 -0
- package/plugin/rules/requiredCollaborators.js +150 -0
- package/plugin/rules/typedDependencies.js +46 -0
- package/plugin/rules/typedInject.js +96 -0
- package/plugin/support.js +377 -0
- package/plugin/vueSources.js +59 -0
- package/typedFeatures.js +104 -0
package/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# @jskit-ai/config-eslint
|
|
2
|
+
|
|
3
|
+
Shared flat ESLint presets for JSKIT projects.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
// eslint.config.mjs
|
|
7
|
+
import {
|
|
8
|
+
baseConfig,
|
|
9
|
+
clientFeaturesConfig,
|
|
10
|
+
formatterConfig,
|
|
11
|
+
nodeConfig,
|
|
12
|
+
sizeLimitsConfig,
|
|
13
|
+
typedFeaturesConfig,
|
|
14
|
+
vueConfig,
|
|
15
|
+
webConfig
|
|
16
|
+
} from "@jskit-ai/config-eslint/server";
|
|
17
|
+
|
|
18
|
+
const SERVER_SOURCES = ["packages/*/src/server/**", "packages/*/src/shared/**", "server/**", "server.js"];
|
|
19
|
+
const CLIENT_SOURCES = ["packages/*/src/client/**", "src/**"];
|
|
20
|
+
const HAND_WRITTEN_SOURCES = ["packages/*/src/**", "src/**", "config/**", "server/**", "server.js", "bin/**"];
|
|
21
|
+
|
|
22
|
+
export default [
|
|
23
|
+
...baseConfig,
|
|
24
|
+
...typedFeaturesConfig(SERVER_SOURCES),
|
|
25
|
+
...sizeLimitsConfig(HAND_WRITTEN_SOURCES),
|
|
26
|
+
...vueConfig,
|
|
27
|
+
...clientFeaturesConfig(CLIENT_SOURCES),
|
|
28
|
+
...webConfig,
|
|
29
|
+
...nodeConfig,
|
|
30
|
+
...formatterConfig([...SERVER_SOURCES, "config/**", "bin/**"])
|
|
31
|
+
];
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- `baseConfig`: ESLint's recommended rules, JSKIT's import policy and, as
|
|
35
|
+
warnings on `**/src/server/**`, the typed-features rules.
|
|
36
|
+
- `typedFeaturesConfig(globs)`: the typed-features rules and the size limits,
|
|
37
|
+
as errors, for the server and shared code the globs select.
|
|
38
|
+
- `clientFeaturesConfig(globs)`: the client rules, as errors, for the client
|
|
39
|
+
code the globs select, Vue components included; list it after `vueConfig`.
|
|
40
|
+
- `sizeLimitsConfig(globs)`: files under 750 lines and functions under 600, as
|
|
41
|
+
errors, for every hand-written file the globs select, client code and Vue
|
|
42
|
+
components included.
|
|
43
|
+
- `formatterConfig(globs)`: the formatter.
|
|
44
|
+
- `nodeConfig`, `webConfig`: Node and browser globals.
|
|
45
|
+
- `vueConfig`: `eslint-plugin-vue`'s recommended rules.
|
|
46
|
+
|
|
47
|
+
## Formatter
|
|
48
|
+
|
|
49
|
+
`formatterConfig(globs)` is the one formatter config of a JSKIT application, as
|
|
50
|
+
errors, for the source files the globs select (tests are not formatted):
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
// eslint.config.mjs
|
|
54
|
+
import { baseConfig, formatterConfig, typedFeaturesConfig } from "@jskit-ai/config-eslint/server";
|
|
55
|
+
|
|
56
|
+
const SERVER_SOURCES = ["packages/*/src/server/**", "packages/*/src/shared/**", "server/**", "server.js"];
|
|
57
|
+
|
|
58
|
+
export default [
|
|
59
|
+
...baseConfig,
|
|
60
|
+
...typedFeaturesConfig(SERVER_SOURCES),
|
|
61
|
+
// Published upgrade scripts never change, so the formatter leaves them alone.
|
|
62
|
+
...formatterConfig(SERVER_SOURCES, { ignores: ["packages/core/src/server/upgrades/**"] })
|
|
63
|
+
];
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- Width 140 for code, comments and JSDoc prose. Strings, template literals,
|
|
67
|
+
regular expressions, URLs, `readable-check: allow` comments, ESLint
|
|
68
|
+
directives and JSDoc type tags may run past it, so long prose stays inline.
|
|
69
|
+
- Semicolons, double quotes (template literals allowed), no trailing commas,
|
|
70
|
+
no space before a named function's parentheses, no repeated or trailing
|
|
71
|
+
spaces.
|
|
72
|
+
- A one-line body without braces stays beside its `if` (`if (!x) return null;`);
|
|
73
|
+
an `if`/`else` chain braces all its bodies or none, and a line holds at most
|
|
74
|
+
two statements.
|
|
75
|
+
- An object whose properties span lines puts each property on its own line,
|
|
76
|
+
with its braces on their own lines; a call whose arguments span lines breaks
|
|
77
|
+
after `(` and before `)`.
|
|
78
|
+
- A braced block spans lines (`1tbs`: `} else {` on the closing brace's line),
|
|
79
|
+
and code is indented by two spaces, `case` inside `switch` by one level and a
|
|
80
|
+
ternary's object, array or function branch past its `?`.
|
|
81
|
+
|
|
82
|
+
`eslint --fix` fixes all of these except the width and the statements per line,
|
|
83
|
+
and its output passes them: a block it braces or an argument list it breaks is
|
|
84
|
+
re-indented in the same run.
|
|
85
|
+
|
|
86
|
+
## Typed features
|
|
87
|
+
|
|
88
|
+
The `jskit` plugin checks the rules of the
|
|
89
|
+
[typed features guide](https://mobily-enterprises.github.io/jskit-ai/guide/framework/typed-features),
|
|
90
|
+
the one place JSKIT explains how server code is written. Its
|
|
91
|
+
[lint rules table](https://mobily-enterprises.github.io/jskit-ai/guide/framework/typed-features#lint-rules)
|
|
92
|
+
lists what each rule reports, every finding ends with a link to the section
|
|
93
|
+
that explains it, and its
|
|
94
|
+
[exceptions](https://mobily-enterprises.github.io/jskit-ai/guide/framework/typed-features#exceptions)
|
|
95
|
+
give the `readable-check: allow` comment and its reason prefixes.
|
|
96
|
+
|
|
97
|
+
`baseConfig` reports the rules as warnings, so adopting a new version does not
|
|
98
|
+
fail a build. `typedFeaturesConfig(globs)` makes them errors for the globs you
|
|
99
|
+
list; an existing application adds each package as it converts it. The rules
|
|
100
|
+
cover every module a capability value is built from, so list a package's
|
|
101
|
+
shared modules too (`packages/billing/src/shared/**`). Test files and `test`,
|
|
102
|
+
`tests`, `client`, `components` and `pages` directories are not checked.
|
|
103
|
+
|
|
104
|
+
## Client features
|
|
105
|
+
|
|
106
|
+
`clientFeaturesConfig(globs, severity = "error")` holds composables, Vue
|
|
107
|
+
components, client factories and client providers to the
|
|
108
|
+
[client features guide](https://mobily-enterprises.github.io/jskit-ai/guide/framework/client-features):
|
|
109
|
+
the typed-features rules that read client code, with `use*` composables
|
|
110
|
+
counted as factories and component templates read for probes and optional
|
|
111
|
+
calls, plus `typed-inject`, `collaborator-props`, `fixed-result-shape`,
|
|
112
|
+
`no-frozen-vue-state`, `released-listeners`, `no-module-state` and
|
|
113
|
+
`eslint-plugin-vue`'s prop and emit rules. The server's action and error rules
|
|
114
|
+
do not apply, a composable is never held to `Object.freeze`, and
|
|
115
|
+
`no-duplicate-helper` compares a client helper with the whole package. Nothing
|
|
116
|
+
changes for client code until a glob selects it, so an existing application
|
|
117
|
+
lists each client tree as it converts it. In a template, one
|
|
118
|
+
`<!-- readable-check: allow <rule> - <reason> -->` above an element excuses
|
|
119
|
+
that element's bindings.
|
|
120
|
+
|
|
121
|
+
`sizeLimitsConfig(globs, { ignores })` holds the rest of the hand-written code
|
|
122
|
+
to the size limits; tests are not checked, and `ignores` leaves generated
|
|
123
|
+
output alone. A generated or static data module is exempt through its first
|
|
124
|
+
line, `/* eslint-disable max-lines -- data: <how it is produced> */`.
|
|
125
|
+
|
|
126
|
+
`one-line-action` leaves CRUD custom actions' `execute({ input, service, … })`
|
|
127
|
+
alone, also when it takes only part of that object (`({ service, context })`):
|
|
128
|
+
that is the CRUD declaration's own contract. `no-duplicate-helper` and
|
|
129
|
+
`named-repeated-type` compare a file with the rest of its package. Both treat a
|
|
130
|
+
pattern's example application (an `example` directory beside a `PATTERN.md`)
|
|
131
|
+
as a package of its own, skip `templates` and `patterns` directories inside a
|
|
132
|
+
package, and leave the agent skill bundle's generated copies under
|
|
133
|
+
`skills/jskit/references/patterns` to their sources. `app-error-code` follows
|
|
134
|
+
the error classes a module declares, and compares codes and sentences within
|
|
135
|
+
one module.
|
package/base.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import js from "@eslint/js";
|
|
2
|
+
import { typedFeaturesConfig } from "./typedFeatures.js";
|
|
2
3
|
|
|
3
4
|
const SOURCE_FILES = "**/*.{js,mjs,cjs,vue}";
|
|
4
5
|
|
|
@@ -43,7 +44,10 @@ const baseConfig = Object.freeze([
|
|
|
43
44
|
}
|
|
44
45
|
]
|
|
45
46
|
}
|
|
46
|
-
}
|
|
47
|
+
},
|
|
48
|
+
// Server code is reported against the typed-features rules; an application
|
|
49
|
+
// makes them errors with typedFeaturesConfig as it converts each package.
|
|
50
|
+
...typedFeaturesConfig(["**/src/server/**"], "warn")
|
|
47
51
|
]);
|
|
48
52
|
|
|
49
53
|
export { baseConfig };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import vuePlugin from "eslint-plugin-vue";
|
|
2
|
+
import vueParser from "vue-eslint-parser";
|
|
3
|
+
import { jskitPlugin } from "./plugin/index.js";
|
|
4
|
+
import { sizeLimitRules } from "./typedFeatures.js";
|
|
5
|
+
|
|
6
|
+
const CLIENT_SOURCE_FILES = "**/*.{js,mjs,vue}";
|
|
7
|
+
const COMPONENT_FILES = "**/*.vue";
|
|
8
|
+
const TEST_FILES = Object.freeze([
|
|
9
|
+
"**/{test,tests}/**",
|
|
10
|
+
"**/*.{test,spec,vitest}.*"
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
// The typed-features rules that read client code, and the client-only rules, at one severity: collaborators of
|
|
14
|
+
// composables, client factories and providers are typed where received and required, never probed, in scripts and
|
|
15
|
+
// templates; injected values take an exported key, a type and no empty default; props a component calls are required
|
|
16
|
+
// and typed by their contract; results keep the shape they are built with and hold Vue state unfrozen; whatever client
|
|
17
|
+
// code starts it releases; no module-level mutable state; one copy of each helper per package; files under 750 lines
|
|
18
|
+
// and functions under 600. The server's action and error rules have no client counterpart, and a composable's result is
|
|
19
|
+
// never held to Object.freeze.
|
|
20
|
+
function clientFeaturesRules(severity) {
|
|
21
|
+
return Object.freeze({
|
|
22
|
+
"jskit/typed-dependencies": severity,
|
|
23
|
+
"jskit/required-collaborators": severity,
|
|
24
|
+
"jskit/no-slot-setter": severity,
|
|
25
|
+
"jskit/no-module-registry": severity,
|
|
26
|
+
"jskit/no-duplicate-helper": severity,
|
|
27
|
+
"jskit/allow-comment-form": severity,
|
|
28
|
+
"jskit/jsdoc-description-inside": severity,
|
|
29
|
+
"jskit/no-dual-shape": severity,
|
|
30
|
+
"jskit/named-repeated-type": severity,
|
|
31
|
+
"jskit/no-factory-spread": severity,
|
|
32
|
+
"jskit/frozen-factory-result": severity,
|
|
33
|
+
"jskit/no-optional-after-default": severity,
|
|
34
|
+
"jskit/one-property-per-line": severity,
|
|
35
|
+
"jskit/no-env-default": severity,
|
|
36
|
+
"jskit/no-test-only-export": severity,
|
|
37
|
+
"jskit/typed-inject": severity,
|
|
38
|
+
"jskit/collaborator-props": severity,
|
|
39
|
+
"jskit/fixed-result-shape": severity,
|
|
40
|
+
"jskit/released-listeners": severity,
|
|
41
|
+
"jskit/no-module-state": severity,
|
|
42
|
+
"jskit/no-frozen-vue-state": severity,
|
|
43
|
+
...sizeLimitRules(severity)
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Every prop has a type and is required or defaulted (never both), and every emitted event is declared.
|
|
48
|
+
function componentRules(severity) {
|
|
49
|
+
return Object.freeze({
|
|
50
|
+
"vue/require-prop-types": severity,
|
|
51
|
+
"vue/require-default-prop": severity,
|
|
52
|
+
"vue/require-valid-default-prop": severity,
|
|
53
|
+
"vue/no-required-prop-with-default": severity,
|
|
54
|
+
"vue/require-explicit-emits": severity
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The typed-features rules for the client source files the globs select (modules and Vue components; tests excluded),
|
|
60
|
+
* as errors unless another severity is given. An application lists each client tree as it converts it, so the rules
|
|
61
|
+
* it already follows stay followed; nothing else changes until it does.
|
|
62
|
+
* @param {string[]} files
|
|
63
|
+
* @param {"error" | "warn" | "off"} [severity]
|
|
64
|
+
*/
|
|
65
|
+
function clientFeaturesConfig(files, severity = "error") {
|
|
66
|
+
if (!Array.isArray(files) || files.length === 0) {
|
|
67
|
+
throw new TypeError("clientFeaturesConfig requires a list of file globs.");
|
|
68
|
+
}
|
|
69
|
+
return Object.freeze([
|
|
70
|
+
Object.freeze({
|
|
71
|
+
files: files.map((pattern) => [pattern, CLIENT_SOURCE_FILES]),
|
|
72
|
+
ignores: TEST_FILES,
|
|
73
|
+
plugins: { jskit: jskitPlugin },
|
|
74
|
+
settings: { jskit: { client: true } },
|
|
75
|
+
rules: clientFeaturesRules(severity)
|
|
76
|
+
}),
|
|
77
|
+
Object.freeze({
|
|
78
|
+
files: files.map((pattern) => [pattern, COMPONENT_FILES]),
|
|
79
|
+
ignores: TEST_FILES,
|
|
80
|
+
plugins: { vue: vuePlugin },
|
|
81
|
+
languageOptions: { parser: vueParser },
|
|
82
|
+
rules: componentRules(severity)
|
|
83
|
+
})
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export { clientFeaturesConfig };
|
package/formatter.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const SOURCE_FILES = "**/*.{js,mjs}";
|
|
2
|
+
// Lines that may run past the width: allow comments, ESLint directives and JSDoc type tags, whose text cannot wrap.
|
|
3
|
+
const LONG_TEXT_PATTERN = "readable-check: allow|eslint-disable|@typedef|@type \\{|@param \\{|@property \\{|@returns \\{";
|
|
4
|
+
const NOT_FORMATTED = Object.freeze([
|
|
5
|
+
"**/{test,tests}/**",
|
|
6
|
+
"**/*.test.*"
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
const FORMATTER_RULES = Object.freeze({
|
|
10
|
+
// Code, comments and JSDoc prose wrap at 140 characters; long prose stays inline in strings and templates.
|
|
11
|
+
"max-len": ["error", {
|
|
12
|
+
code: 140,
|
|
13
|
+
ignoreUrls: true,
|
|
14
|
+
ignoreStrings: true,
|
|
15
|
+
ignoreTemplateLiterals: true,
|
|
16
|
+
ignoreRegExpLiterals: true,
|
|
17
|
+
ignorePattern: LONG_TEXT_PATTERN
|
|
18
|
+
}],
|
|
19
|
+
semi: ["error", "always"],
|
|
20
|
+
quotes: ["error", "double", { avoidEscape: true, allowTemplateLiterals: true }],
|
|
21
|
+
"comma-dangle": ["error", "never"],
|
|
22
|
+
"space-before-function-paren": ["error", { anonymous: "ignore", named: "never", asyncArrow: "ignore" }],
|
|
23
|
+
// One layout: a one-line `if (x) return y;`, or braces; one property per line once an object spans lines.
|
|
24
|
+
curly: ["error", "multi-line", "consistent"],
|
|
25
|
+
"nonblock-statement-body-position": ["error", "beside"],
|
|
26
|
+
"object-curly-newline": ["error", { multiline: true, consistent: true }],
|
|
27
|
+
"object-property-newline": ["error", { allowAllPropertiesOnSameLine: true }],
|
|
28
|
+
"function-paren-newline": ["error", "multiline-arguments"],
|
|
29
|
+
"max-statements-per-line": ["error", { max: 2 }],
|
|
30
|
+
"no-multi-spaces": "error",
|
|
31
|
+
"no-trailing-spaces": "error",
|
|
32
|
+
// A braced block spans lines, `} else {` stays on the closing brace's line, and two-space indentation re-indents
|
|
33
|
+
// what the other fixes break onto new lines. A ternary's object, array or function branch is indented past its `?`.
|
|
34
|
+
"brace-style": ["error", "1tbs"],
|
|
35
|
+
"block-spacing": ["error", "always"],
|
|
36
|
+
indent: ["error", 2, { SwitchCase: 1, offsetTernaryExpressions: true }]
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The one formatter config of a JSKIT application, as errors, for the source files the globs select: width 140,
|
|
41
|
+
* semicolons, double quotes, no trailing commas, one property per line in an object that spans lines, a one-line
|
|
42
|
+
* `if` only without braces, braced blocks on their own lines and two-space indentation. Tests are not formatted.
|
|
43
|
+
* @param {string[]} files
|
|
44
|
+
* @param {{ ignores?: string[] }} [options] Files the formatter never touches, such as published scripts that must not
|
|
45
|
+
* change.
|
|
46
|
+
*/
|
|
47
|
+
function formatterConfig(files, { ignores = [] } = {}) {
|
|
48
|
+
if (!Array.isArray(files) || files.length === 0) {
|
|
49
|
+
throw new TypeError("formatterConfig requires a list of file globs.");
|
|
50
|
+
}
|
|
51
|
+
return Object.freeze([
|
|
52
|
+
Object.freeze({
|
|
53
|
+
files: files.map((pattern) => [pattern, SOURCE_FILES]),
|
|
54
|
+
ignores: [...NOT_FORMATTED, ...ignores],
|
|
55
|
+
rules: FORMATTER_RULES
|
|
56
|
+
})
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { formatterConfig };
|
package/index.js
CHANGED
|
@@ -2,3 +2,7 @@ export { baseConfig } from "./base.js";
|
|
|
2
2
|
export { nodeConfig } from "./node.js";
|
|
3
3
|
export { webConfig } from "./web.js";
|
|
4
4
|
export { vueConfig } from "./vue.js";
|
|
5
|
+
export { sizeLimitsConfig, typedFeaturesConfig } from "./typedFeatures.js";
|
|
6
|
+
export { clientFeaturesConfig } from "./clientFeatures.js";
|
|
7
|
+
export { formatterConfig } from "./formatter.js";
|
|
8
|
+
export { jskitPlugin } from "./plugin/index.js";
|
package/package.json
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/config-eslint",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.243",
|
|
4
4
|
"description": "Shared flat ESLint presets for JSKIT projects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"src",
|
|
8
|
+
"plugin",
|
|
8
9
|
"base.js",
|
|
9
10
|
"index.js",
|
|
10
11
|
"node.js",
|
|
11
12
|
"web.js",
|
|
12
13
|
"vue.js",
|
|
14
|
+
"typedFeatures.js",
|
|
15
|
+
"clientFeatures.js",
|
|
16
|
+
"formatter.js",
|
|
13
17
|
"README.md"
|
|
14
18
|
],
|
|
15
19
|
"scripts": {
|
|
@@ -21,6 +25,9 @@
|
|
|
21
25
|
"./node": "./node.js",
|
|
22
26
|
"./web": "./web.js",
|
|
23
27
|
"./vue": "./vue.js",
|
|
28
|
+
"./typed-features": "./typedFeatures.js",
|
|
29
|
+
"./client-features": "./clientFeatures.js",
|
|
30
|
+
"./formatter": "./formatter.js",
|
|
24
31
|
"./server": "./src/server/index.js",
|
|
25
32
|
"./client": "./src/client/index.js"
|
|
26
33
|
},
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { isFreezeCall } from "./support.js";
|
|
2
|
+
|
|
3
|
+
// What the factory rules share: a function's defined name, what it returns,
|
|
4
|
+
// and whether an object it returns holds behaviour or only data.
|
|
5
|
+
|
|
6
|
+
const CREATE_NAME = /^create[A-Z]/u;
|
|
7
|
+
const FUNCTION_TYPES = new Set(["ArrowFunctionExpression", "FunctionExpression", "FunctionDeclaration"]);
|
|
8
|
+
// Vue's calls that make reactive state. A value holding their results must stay extensible: reactive() returns a
|
|
9
|
+
// frozen object unproxied, so its refs stop unwrapping, and proxyRefs() (which Vue also applies to a setup() result
|
|
10
|
+
// and to defineExpose()) throws reading a ref of a frozen object.
|
|
11
|
+
const VUE_STATE_CALLS = new Set([
|
|
12
|
+
"ref", "shallowRef", "computed", "reactive", "shallowReactive", "readonly", "shallowReadonly", "toRef", "toRefs",
|
|
13
|
+
"customRef", "useTemplateRef", "useModel", "defineModel"
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
// Whether a call makes Vue reactive state.
|
|
17
|
+
function isVueStateCall(node) {
|
|
18
|
+
return node?.type === "CallExpression" && node.callee.type === "Identifier" && VUE_STATE_CALLS.has(node.callee.name);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Whether a function makes Vue reactive state anywhere in its body: a composable or a client runtime, whose result
|
|
22
|
+
// callers may hand to reactive() or proxyRefs().
|
|
23
|
+
function makesVueState(sourceCode, fn) {
|
|
24
|
+
let found = false;
|
|
25
|
+
const visit = (node) => {
|
|
26
|
+
if (found || !node || typeof node.type !== "string") {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (isVueStateCall(node)) {
|
|
30
|
+
found = true;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
for (const key of sourceCode.visitorKeys[node.type] || []) {
|
|
34
|
+
[node[key]].flat().forEach(visit);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
visit(fn.body);
|
|
38
|
+
return found;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The name a function is defined under: its own, its variable's, its property's
|
|
42
|
+
// or the member it is assigned to.
|
|
43
|
+
function definedName(fn) {
|
|
44
|
+
if (fn.id?.name) {
|
|
45
|
+
return fn.id.name;
|
|
46
|
+
}
|
|
47
|
+
const parent = fn.parent;
|
|
48
|
+
if (parent?.type === "VariableDeclarator" && parent.init === fn) {
|
|
49
|
+
return parent.id.name || "";
|
|
50
|
+
}
|
|
51
|
+
if ((parent?.type === "Property" || parent?.type === "MethodDefinition") && parent.value === fn) {
|
|
52
|
+
return parent.key?.name || parent.key?.value || "";
|
|
53
|
+
}
|
|
54
|
+
if (parent?.type === "AssignmentExpression" && parent.right === fn) {
|
|
55
|
+
return parent.left.property?.name || parent.left.name || "";
|
|
56
|
+
}
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The function a return statement returns from.
|
|
61
|
+
function enclosingFunction(sourceCode, node) {
|
|
62
|
+
return sourceCode.getAncestors(node).findLast((ancestor) => FUNCTION_TYPES.has(ancestor.type)) || null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The expressions a function returns: each return statement's argument, or an
|
|
66
|
+
// arrow's expression body. Used from a ReturnStatement or an arrow visitor.
|
|
67
|
+
function returnedFrom(sourceCode, node) {
|
|
68
|
+
if (node.type === "ReturnStatement") {
|
|
69
|
+
const fn = enclosingFunction(sourceCode, node);
|
|
70
|
+
return fn && node.argument ? { fn, returned: node.argument } : null;
|
|
71
|
+
}
|
|
72
|
+
return node.body.type === "BlockStatement" ? null : { fn: node, returned: node.body };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function findVariable(scope, name) {
|
|
76
|
+
for (let current = scope; current; current = current.upper) {
|
|
77
|
+
const variable = current.set.get(name);
|
|
78
|
+
if (variable) {
|
|
79
|
+
return variable;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// The single definition of the variable an identifier reads, or null.
|
|
86
|
+
function definitionOf(sourceCode, identifier) {
|
|
87
|
+
const variable = findVariable(sourceCode.getScope(identifier), identifier.name);
|
|
88
|
+
return variable?.defs.length === 1 ? variable.defs[0] : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isInlineFunction(property) {
|
|
92
|
+
return property.type === "Property" && property.kind === "init" && (property.method || FUNCTION_TYPES.has(property.value.type));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// A member that refers to a function the module or factory defines.
|
|
96
|
+
function refersToFunction(sourceCode, property) {
|
|
97
|
+
if (property.type !== "Property" || property.value.type !== "Identifier") {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const definition = definitionOf(sourceCode, property.value);
|
|
101
|
+
return definition?.type === "FunctionName" ||
|
|
102
|
+
(definition?.type === "Variable" && FUNCTION_TYPES.has(definition.node.init?.type));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// An object holds behaviour when a member is a function, an accessor or a
|
|
106
|
+
// reference to a function; otherwise it is data.
|
|
107
|
+
function holdsBehaviour(sourceCode, object) {
|
|
108
|
+
return object.properties.some((property) => isInlineFunction(property) ||
|
|
109
|
+
(property.type === "Property" && property.kind !== "init") || refersToFunction(sourceCode, property));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// The function a factory name refers to in this module, or null.
|
|
113
|
+
function localFunction(sourceCode, identifier) {
|
|
114
|
+
const definition = definitionOf(sourceCode, identifier);
|
|
115
|
+
if (definition?.type === "FunctionName") {
|
|
116
|
+
return definition.node;
|
|
117
|
+
}
|
|
118
|
+
const init = definition?.type === "Variable" ? definition.node.init : null;
|
|
119
|
+
return FUNCTION_TYPES.has(init?.type) ? init : null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// A module function that returns only data: every value it returns is an
|
|
123
|
+
// object literal, frozen or not, without behaviour.
|
|
124
|
+
function returnsOnlyData(sourceCode, fn) {
|
|
125
|
+
const returned = [];
|
|
126
|
+
if (fn.body.type !== "BlockStatement") {
|
|
127
|
+
returned.push(fn.body);
|
|
128
|
+
} else {
|
|
129
|
+
const visit = (node) => {
|
|
130
|
+
if (!node || typeof node.type !== "string" || (node !== fn && FUNCTION_TYPES.has(node.type))) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (node.type === "ReturnStatement") {
|
|
134
|
+
returned.push(node.argument);
|
|
135
|
+
}
|
|
136
|
+
for (const key of sourceCode.visitorKeys[node.type] || []) {
|
|
137
|
+
[node[key]].flat().forEach(visit);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
visit(fn.body);
|
|
141
|
+
}
|
|
142
|
+
return returned.length > 0 && returned.every((value) => {
|
|
143
|
+
const object = isFreezeCall(value) ? value.arguments[0] : value;
|
|
144
|
+
return object?.type === "ObjectExpression" && !holdsBehaviour(sourceCode, object);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export {
|
|
149
|
+
CREATE_NAME,
|
|
150
|
+
FUNCTION_TYPES,
|
|
151
|
+
VUE_STATE_CALLS,
|
|
152
|
+
definedName,
|
|
153
|
+
definitionOf,
|
|
154
|
+
enclosingFunction,
|
|
155
|
+
holdsBehaviour,
|
|
156
|
+
isInlineFunction,
|
|
157
|
+
isVueStateCall,
|
|
158
|
+
localFunction,
|
|
159
|
+
makesVueState,
|
|
160
|
+
returnedFrom,
|
|
161
|
+
returnsOnlyData
|
|
162
|
+
};
|
package/plugin/index.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { actionCreator } from "./rules/actionCreator.js";
|
|
2
|
+
import { actionExecuteShape } from "./rules/actionExecuteShape.js";
|
|
3
|
+
import { allowCommentForm } from "./rules/allowCommentForm.js";
|
|
4
|
+
import { appErrorCode } from "./rules/appErrorCode.js";
|
|
5
|
+
import { collaboratorProps } from "./rules/collaboratorProps.js";
|
|
6
|
+
import { fixedResultShape } from "./rules/fixedResultShape.js";
|
|
7
|
+
import { frozenFactoryResult } from "./rules/frozenFactoryResult.js";
|
|
8
|
+
import { noOptionalAfterDefault } from "./rules/noOptionalAfterDefault.js";
|
|
9
|
+
import { jsdocDescriptionInside } from "./rules/jsdocDescriptionInside.js";
|
|
10
|
+
import { namedRepeatedType } from "./rules/namedRepeatedType.js";
|
|
11
|
+
import { noDualShape } from "./rules/noDualShape.js";
|
|
12
|
+
import { noDuplicateHelper } from "./rules/noDuplicateHelper.js";
|
|
13
|
+
import { noEnvDefault } from "./rules/noEnvDefault.js";
|
|
14
|
+
import { noFactorySpread } from "./rules/noFactorySpread.js";
|
|
15
|
+
import { noFrozenVueState } from "./rules/noFrozenVueState.js";
|
|
16
|
+
import { noIdempotency } from "./rules/noIdempotency.js";
|
|
17
|
+
import { noModuleRegistry } from "./rules/noModuleRegistry.js";
|
|
18
|
+
import { noModuleState } from "./rules/noModuleState.js";
|
|
19
|
+
import { noPatchedError } from "./rules/noPatchedError.js";
|
|
20
|
+
import { noSlotSetter } from "./rules/noSlotSetter.js";
|
|
21
|
+
import { noTestOnlyExport } from "./rules/noTestOnlyExport.js";
|
|
22
|
+
import { oneLineAction } from "./rules/oneLineAction.js";
|
|
23
|
+
import { onePropertyPerLine } from "./rules/onePropertyPerLine.js";
|
|
24
|
+
import { releasedListeners } from "./rules/releasedListeners.js";
|
|
25
|
+
import { requiredCollaborators } from "./rules/requiredCollaborators.js";
|
|
26
|
+
import { typedDependencies } from "./rules/typedDependencies.js";
|
|
27
|
+
import { typedInject } from "./rules/typedInject.js";
|
|
28
|
+
|
|
29
|
+
// The `jskit` ESLint plugin: the typed-features rules, by name, server and client. Each rule's messages link to the
|
|
30
|
+
// guide section that explains it.
|
|
31
|
+
const jskitPlugin = Object.freeze({
|
|
32
|
+
meta: Object.freeze({ name: "@jskit-ai/config-eslint/jskit" }),
|
|
33
|
+
rules: Object.freeze({
|
|
34
|
+
"typed-dependencies": typedDependencies,
|
|
35
|
+
"required-collaborators": requiredCollaborators,
|
|
36
|
+
"no-slot-setter": noSlotSetter,
|
|
37
|
+
"no-module-registry": noModuleRegistry,
|
|
38
|
+
"one-line-action": oneLineAction,
|
|
39
|
+
"no-duplicate-helper": noDuplicateHelper,
|
|
40
|
+
"allow-comment-form": allowCommentForm,
|
|
41
|
+
"jsdoc-description-inside": jsdocDescriptionInside,
|
|
42
|
+
"no-dual-shape": noDualShape,
|
|
43
|
+
"named-repeated-type": namedRepeatedType,
|
|
44
|
+
"action-execute-shape": actionExecuteShape,
|
|
45
|
+
"no-factory-spread": noFactorySpread,
|
|
46
|
+
"frozen-factory-result": frozenFactoryResult,
|
|
47
|
+
"no-optional-after-default": noOptionalAfterDefault,
|
|
48
|
+
"one-property-per-line": onePropertyPerLine,
|
|
49
|
+
"action-creator": actionCreator,
|
|
50
|
+
"no-idempotency": noIdempotency,
|
|
51
|
+
"app-error-code": appErrorCode,
|
|
52
|
+
"no-patched-error": noPatchedError,
|
|
53
|
+
"no-env-default": noEnvDefault,
|
|
54
|
+
"no-test-only-export": noTestOnlyExport,
|
|
55
|
+
"typed-inject": typedInject,
|
|
56
|
+
"collaborator-props": collaboratorProps,
|
|
57
|
+
"fixed-result-shape": fixedResultShape,
|
|
58
|
+
"released-listeners": releasedListeners,
|
|
59
|
+
"no-module-state": noModuleState,
|
|
60
|
+
"no-frozen-vue-state": noFrozenVueState
|
|
61
|
+
})
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export { jskitPlugin };
|