@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.
@@ -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.
@@ -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.
@@ -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
- fixtures: {
29
- file: 'app/a.ts',
30
- bad: 'window.addEventListener("resize", onResize)',
31
- good: 'useEventListener(window, "resize", onResize)',
32
- },
33
- check: ({ file, code }) => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brighterly/lib-fe-codestyle",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "description": "Shared invariant-validator engine, rule pack and ESLint config for Brighterly Nuxt frontend apps",