@brighterly/lib-fe-codestyle 1.0.1 → 1.0.2
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/dist/engine/ast.d.ts +8 -0
- package/dist/engine/ast.js +51 -0
- package/dist/rules/convention.js +19 -6
- package/package.json +1 -1
package/dist/engine/ast.d.ts
CHANGED
|
@@ -22,6 +22,14 @@ export declare const findJsonParseCalls: (file: string, source: string) => Promi
|
|
|
22
22
|
line: number;
|
|
23
23
|
guarded: boolean;
|
|
24
24
|
}[]>;
|
|
25
|
+
/**
|
|
26
|
+
* The 1-based lines of a file that run inside a component lifetime: the body of a `<script setup>`
|
|
27
|
+
* block, and the body of any function named `use…` (a composable) or `setup` (Options API /
|
|
28
|
+
* `defineComponent`), including everything nested inside them. A store action, a plugin, a plain
|
|
29
|
+
* helper or module-level code is outside: VueUse's automatic cleanup has no scope to attach to there,
|
|
30
|
+
* so a rule that recommends a VueUse composable must stay silent on those lines.
|
|
31
|
+
*/
|
|
32
|
+
export declare const componentScopeLines: (file: string, source: string) => Promise<Set<number>>;
|
|
25
33
|
/**
|
|
26
34
|
* Every static import that survives compilation: default, named, namespace and bare side-effect
|
|
27
35
|
* imports plus `export … from` re-exports, in .ts files and in every script block of a .vue file.
|
package/dist/engine/ast.js
CHANGED
|
@@ -83,6 +83,57 @@ export const findJsonParseCalls = async (file, source) => {
|
|
|
83
83
|
}
|
|
84
84
|
return out;
|
|
85
85
|
};
|
|
86
|
+
/**
|
|
87
|
+
* The 1-based lines of a file that run inside a component lifetime: the body of a `<script setup>`
|
|
88
|
+
* block, and the body of any function named `use…` (a composable) or `setup` (Options API /
|
|
89
|
+
* `defineComponent`), including everything nested inside them. A store action, a plugin, a plain
|
|
90
|
+
* helper or module-level code is outside: VueUse's automatic cleanup has no scope to attach to there,
|
|
91
|
+
* so a rule that recommends a VueUse composable must stay silent on those lines.
|
|
92
|
+
*/
|
|
93
|
+
export const componentScopeLines = async (file, source) => {
|
|
94
|
+
const t = await ts();
|
|
95
|
+
const lines = new Set();
|
|
96
|
+
const add = (from, to) => { for (let l = from; l <= to; l++)
|
|
97
|
+
lines.add(l); };
|
|
98
|
+
if (file.endsWith('.vue')) {
|
|
99
|
+
const re = /<script\b([^>]*)>([\s\S]*?)<\/script>/g;
|
|
100
|
+
let m;
|
|
101
|
+
while ((m = re.exec(source)) !== null) {
|
|
102
|
+
if (!/\bsetup\b/.test(m[1] ?? '') || m[2] === undefined)
|
|
103
|
+
continue;
|
|
104
|
+
const bodyStart = m.index + m[0].length - m[2].length - '</script>'.length;
|
|
105
|
+
const first = source.slice(0, bodyStart).split('\n').length;
|
|
106
|
+
add(first, first + m[2].split('\n').length - 1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const SCOPED = /^(?:use[A-Z0-9]|setup$)/;
|
|
110
|
+
const nameOf = (node) => {
|
|
111
|
+
if ((t.isFunctionDeclaration(node) || t.isMethodDeclaration(node) || t.isFunctionExpression(node)) && node.name && t.isIdentifier(node.name))
|
|
112
|
+
return node.name.text;
|
|
113
|
+
const parent = node.parent;
|
|
114
|
+
if (!parent)
|
|
115
|
+
return null;
|
|
116
|
+
if (t.isVariableDeclaration(parent) && t.isIdentifier(parent.name))
|
|
117
|
+
return parent.name.text;
|
|
118
|
+
if (t.isPropertyAssignment(parent) && (t.isIdentifier(parent.name) || t.isStringLiteral(parent.name)))
|
|
119
|
+
return parent.name.text;
|
|
120
|
+
return null;
|
|
121
|
+
};
|
|
122
|
+
for (const script of scriptsOf(file, source)) {
|
|
123
|
+
const sf = t.createSourceFile(file, script.code, t.ScriptTarget.Latest, true);
|
|
124
|
+
const lineOf = (pos) => sf.getLineAndCharacterOfPosition(pos).line + 1 + script.lineOffset;
|
|
125
|
+
const visit = (node) => {
|
|
126
|
+
const isFn = t.isFunctionDeclaration(node) || t.isFunctionExpression(node) || t.isArrowFunction(node) || t.isMethodDeclaration(node);
|
|
127
|
+
if (isFn && SCOPED.test(nameOf(node) ?? '')) {
|
|
128
|
+
add(lineOf(node.getStart(sf)), lineOf(node.getEnd()));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
t.forEachChild(node, visit);
|
|
132
|
+
};
|
|
133
|
+
visit(sf);
|
|
134
|
+
}
|
|
135
|
+
return lines;
|
|
136
|
+
};
|
|
86
137
|
/**
|
|
87
138
|
* Every static import that survives compilation: default, named, namespace and bare side-effect
|
|
88
139
|
* imports plus `export … from` re-exports, in .ts files and in every script block of a .vue file.
|
package/dist/rules/convention.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* They ship at `warn` so each consuming app can measure what they find before anyone
|
|
17
17
|
* decides to block on them. Promoting one to `error` is a major version bump.
|
|
18
18
|
*/
|
|
19
|
+
import { componentScopeLines } from '../engine/ast.js';
|
|
19
20
|
import { eachLine, setting } from './shared.js';
|
|
20
21
|
const vueUseFirst = {
|
|
21
22
|
name: 'vueuse-first',
|
|
@@ -25,12 +26,22 @@ const vueUseFirst = {
|
|
|
25
26
|
fix: 'use the VueUse composable named in the message, imported explicitly from @vueuse/core',
|
|
26
27
|
scope: ['app/**/*.{ts,vue}'],
|
|
27
28
|
exclude: ['app/tests/**', 'app/test/**'],
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
async: true,
|
|
30
|
+
// Only lines inside a component lifetime count (`<script setup>`, a `use…` composable, a `setup()`
|
|
31
|
+
// method). A store action, a plugin or a plain helper has no effect scope, so the VueUse composable
|
|
32
|
+
// the message recommends would leak there — those lines are silent by construction.
|
|
33
|
+
fixtures: [
|
|
34
|
+
{ file: 'app/composables/a.ts', bad: 'export const useA = () => {\n window.addEventListener("resize", onResize)\n}', good: 'export const useA = () => {\n useEventListener(window, "resize", onResize)\n}' },
|
|
35
|
+
{ file: 'app/composables/b.ts', bad: 'export function useB() {\n const run = () => setTimeout(tick, 10)\n return { run }\n}' },
|
|
36
|
+
{ file: 'app/components/A.vue', bad: '<script setup lang="ts">\nsetTimeout(tick, 10)\n</script>', good: '<script setup lang="ts">\nuseTimeoutFn(tick, 10)\n</script>' },
|
|
37
|
+
{ file: 'app/components/B.vue', bad: '<script lang="ts">\nexport default defineComponent({\n setup() {\n setInterval(tick, 10)\n },\n})\n</script>' },
|
|
38
|
+
{ file: 'app/stores/quiz/actions.ts', good: 'export const actions = {\n setLazyStep(payload) {\n setTimeout(() => setStep(payload), 300)\n },\n}' },
|
|
39
|
+
{ file: 'app/composables/tools/scroll.ts', good: 'export const scrollIntoViewIfHidden = (el) => {\n new IntersectionObserver(cb).observe(el)\n}' },
|
|
40
|
+
{ file: 'app/composables/tools/sdk.ts', good: 'const init = () => {\n load().catch(() => setTimeout(init, 800))\n}' },
|
|
41
|
+
{ file: 'app/plugins/axios.client.ts', good: 'export default defineNuxtPlugin(() => {\n setTimeout(refresh, 1000)\n})' },
|
|
42
|
+
],
|
|
43
|
+
check: async ({ file, source, code }) => {
|
|
44
|
+
const scoped = await componentScopeLines(file, source);
|
|
34
45
|
const map = [
|
|
35
46
|
[/new\s+IntersectionObserver\s*\(/, 'useIntersectionObserver'],
|
|
36
47
|
[/new\s+ResizeObserver\s*\(/, 'useResizeObserver'],
|
|
@@ -45,6 +56,8 @@ const vueUseFirst = {
|
|
|
45
56
|
[/\b(?:window\s*\.\s*)?setTimeout\s*\(/, 'useTimeoutFn'],
|
|
46
57
|
];
|
|
47
58
|
return eachLine(code, (line, n) => {
|
|
59
|
+
if (!scoped.has(n))
|
|
60
|
+
return null;
|
|
48
61
|
for (const [re, fn] of map) {
|
|
49
62
|
if (re.test(line))
|
|
50
63
|
return { file, line: n, message: `hand-rolled browser API — use ${fn} from @vueuse/core` };
|
package/package.json
CHANGED