eslint-plugin-ocavue 0.0.1 → 0.1.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 CHANGED
@@ -2,43 +2,52 @@
2
2
 
3
3
  [![NPM version](https://img.shields.io/npm/v/eslint-plugin-ocavue?color=a1b858&label=)](https://www.npmjs.com/package/eslint-plugin-ocavue)
4
4
 
5
- A minimal [TypeScript](https://www.typescriptlang.org/) library starter with bundling, testing, linting, formatting, and automated npm releases, all preconfigured.
5
+ Custom [ESLint](https://eslint.org/) rules from [ocavue](https://github.com/ocavue).
6
6
 
7
- > **Using this template?** Click [Use this template](https://github.com/ocavue/starter-ts/generate), rename the package in `package.json`, and write your code in `src/index.ts`.
7
+ ## Usage
8
8
 
9
- ## Project structure
9
+ Requires ESLint 10 with flat config.
10
10
 
11
- - `src/`: source code, with co-located [Vitest](https://vitest.dev/) tests (`*.test.ts`)
12
- - `dist/`: bundled output (ESM + type declarations), built by [tsdown](https://tsdown.dev/)
11
+ ```sh
12
+ pnpm add -D eslint-plugin-ocavue
13
+ ```
13
14
 
14
- ## Scripts
15
+ ```js
16
+ // eslint.config.js
17
+ import ocavue from 'eslint-plugin-ocavue'
15
18
 
16
- Local development needs [Node.js](https://nodejs.org/) v22+ and [pnpm](https://pnpm.io/).
19
+ export default [
20
+ {
21
+ plugins: { ocavue },
22
+ rules: {
23
+ 'ocavue/no-implicit-arrow-linebreak': 'error',
24
+ },
25
+ },
26
+ ]
27
+ ```
17
28
 
18
- | Command | Description |
19
- | ---------------- | -------------------------------------------------------------------------------------------------------------------------- |
20
- | `pnpm dev` | Rebuild on change (`tsdown --watch`) |
21
- | `pnpm build` | Bundle the library to `dist/` |
22
- | `pnpm test` | Run tests with [Vitest](https://vitest.dev/) |
23
- | `pnpm lint` | Lint with [ESLint](https://eslint.org/), [oxfmt](https://oxc.rs/docs/guide/usage/formatter), and [knip](https://knip.dev/) |
24
- | `pnpm fix` | Auto-fix formatting and lint issues |
25
- | `pnpm typecheck` | Type-check with `tsc` |
29
+ ## Rules
26
30
 
27
- ## Publishing
31
+ ### `no-implicit-arrow-linebreak`
28
32
 
29
- Releases are automated with [release-please](https://github.com/googleapis/release-please) and npm [trusted publishing](https://docs.npmjs.com/trusted-publishers/) (OIDC). No tokens to manage.
33
+ Disallow a linebreak between `=>` and an implicit arrow function body. Unlike [`@stylistic/implicit-arrow-linebreak`](https://eslint.style/rules/implicit-arrow-linebreak), whose fixer joins the two lines into one (which Prettier then breaks again whenever the joined line exceeds `printWidth`, so `eslint --fix` and the formatter never converge), this rule's fixer converts the body to a block body with `return`. A block body is a stable shape for Prettier and oxfmt, so one round of `eslint --fix` plus the formatter converges.
30
34
 
31
- 1. **First release (manual).** OIDC can't create a brand-new package, so publish the first version by hand:
35
+ ```js
36
+ // Incorrect
37
+ const items = Array.from({ length: count }, (_, index) =>
38
+ createItem(index === 0 ? 'first item label' : `item number ${index}`),
39
+ )
32
40
 
33
- ```bash
34
- pnpm login
35
- pnpm build
36
- pnpm publish
37
- ```
41
+ // Correct
42
+ const short = (index) => createItem(`item number ${index}`)
38
43
 
39
- 2. **Enable OIDC.** On [npmjs.com](https://www.npmjs.com/), open the package → **Settings → Trusted Publisher → GitHub Actions**, and set the workflow filename to `release.yml`.
44
+ // Correct (what the fixer produces, after formatting)
45
+ const items = Array.from({ length: count }, (_, index) => {
46
+ return createItem(index === 0 ? 'first item label' : `item number ${index}`)
47
+ })
48
+ ```
40
49
 
41
- 3. **Future releases (automatic).** Push [Conventional Commits](https://www.conventionalcommits.org/) (`fix:`, `feat:`, …) to your default branch (usually `master` or `main`); [release-please](https://github.com/googleapis/release-please) opens a release PR that bumps the version, updates the changelog, and publishes on merge.
50
+ When a comment sits between `=>` and the body, the rule reports without autofixing so the fix never drops a comment.
42
51
 
43
52
  ## Sponsors
44
53
 
package/dist/index.d.ts CHANGED
@@ -1,6 +1,13 @@
1
1
  //#region src/index.d.ts
2
- declare const one = 1;
3
- declare const two = 2;
2
+ declare const plugin: {
3
+ meta: {
4
+ name: string;
5
+ version: string;
6
+ };
7
+ rules: {
8
+ 'no-implicit-arrow-linebreak': import("eslint").Rule.RuleModule;
9
+ };
10
+ };
4
11
  //#endregion
5
- export { one, two };
12
+ export { plugin as default };
6
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";cAAa;cACA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";cAMM"}
package/dist/index.js CHANGED
@@ -1,7 +1,45 @@
1
+ //#endregion
1
2
  //#region src/index.ts
2
- const one = 1;
3
- const two = 2;
3
+ const plugin = {
4
+ meta: {
5
+ name: "eslint-plugin-ocavue",
6
+ version: "0.1.0"
7
+ },
8
+ rules: { "no-implicit-arrow-linebreak": {
9
+ meta: {
10
+ type: "layout",
11
+ fixable: "code",
12
+ docs: {
13
+ description: "disallow a linebreak between `=>` and an implicit arrow function body, fixing it to a block body with `return`",
14
+ url: "https://github.com/ocavue/eslint-plugin-ocavue#no-implicit-arrow-linebreak"
15
+ },
16
+ messages: { linebreak: "Implicit arrow body must not start on a new line. Use a block body with `return` instead." },
17
+ schema: []
18
+ },
19
+ create(context) {
20
+ const sourceCode = context.sourceCode;
21
+ return { ArrowFunctionExpression(node) {
22
+ if (node.body.type === "BlockStatement") return;
23
+ const arrowToken = sourceCode.getTokenBefore(node.body, { filter: (token) => token.type === "Punctuator" && token.value === "=>" });
24
+ if (!arrowToken) return;
25
+ const firstBodyToken = sourceCode.getTokenAfter(arrowToken);
26
+ const lastToken = sourceCode.getLastToken(node);
27
+ if (!firstBodyToken || !lastToken) return;
28
+ if (arrowToken.loc.end.line === firstBodyToken.loc.start.line) return;
29
+ const canFix = !sourceCode.commentsExistBetween(arrowToken, firstBodyToken);
30
+ context.report({
31
+ node: node.body,
32
+ messageId: "linebreak",
33
+ fix: canFix ? (fixer) => {
34
+ const bodyText = sourceCode.text.slice(firstBodyToken.range[0], lastToken.range[1]);
35
+ return fixer.replaceTextRange([arrowToken.range[1], lastToken.range[1]], ` { return ${bodyText} }`);
36
+ } : void 0
37
+ });
38
+ } };
39
+ }
40
+ } }
41
+ };
4
42
  //#endregion
5
- export { one, two };
43
+ export { plugin as default };
6
44
 
7
45
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["export const one = 1\nexport const two = 2\n"],"mappings":";AAAA,MAAa,MAAM;AACnB,MAAa,MAAM"}
1
+ {"version":3,"file":"index.js","names":["noImplicitArrowLinebreak"],"sources":["../package.json","../src/rules/no-implicit-arrow-linebreak.ts","../src/index.ts"],"sourcesContent":["","import type { Rule } from 'eslint'\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'layout',\n fixable: 'code',\n docs: {\n description:\n 'disallow a linebreak between `=>` and an implicit arrow function body, fixing it to a block body with `return`',\n url: 'https://github.com/ocavue/eslint-plugin-ocavue#no-implicit-arrow-linebreak',\n },\n messages: {\n linebreak:\n 'Implicit arrow body must not start on a new line. Use a block body with `return` instead.',\n },\n schema: [],\n },\n create(context) {\n const sourceCode = context.sourceCode\n return {\n ArrowFunctionExpression(node) {\n if (node.body.type === 'BlockStatement') {\n return\n }\n\n const arrowToken = sourceCode.getTokenBefore(node.body, {\n filter: (token) => token.type === 'Punctuator' && token.value === '=>',\n })\n if (!arrowToken) {\n return\n }\n const firstBodyToken = sourceCode.getTokenAfter(arrowToken)\n // The last token of the arrow function. When the body is wrapped in\n // parentheses, this is the closing `)`, so the fix keeps the parens.\n const lastToken = sourceCode.getLastToken(node)\n if (!firstBodyToken || !lastToken) {\n return\n }\n if (arrowToken.loc.end.line === firstBodyToken.loc.start.line) {\n return\n }\n\n // The fix rewrites the region between `=>` and the body, so bail out\n // of fixing (but still report) if that region contains comments.\n const canFix = !sourceCode.commentsExistBetween(arrowToken, firstBodyToken)\n\n context.report({\n node: node.body,\n messageId: 'linebreak',\n fix: canFix\n ? (fixer) => {\n const bodyText = sourceCode.text.slice(firstBodyToken.range[0], lastToken.range[1])\n return fixer.replaceTextRange(\n [arrowToken.range[1], lastToken.range[1]],\n ` { return ${bodyText} }`,\n )\n }\n : undefined,\n })\n },\n }\n },\n}\n\nexport default rule\n","import type { ESLint } from 'eslint'\n\nimport { version } from '../package.json'\n\nimport noImplicitArrowLinebreak from './rules/no-implicit-arrow-linebreak'\n\nconst plugin = {\n meta: {\n name: 'eslint-plugin-ocavue',\n version,\n },\n rules: {\n 'no-implicit-arrow-linebreak': noImplicitArrowLinebreak,\n },\n} satisfies ESLint.Plugin\n\nexport default plugin\n"],"mappings":";;AEMA,MAAM,SAAS;CACb,MAAM;EACJ,MAAM;EACN;CACF;CACA,OAAO,EACL,+BAA+BA;EDTjC,MAAM;GACJ,MAAM;GACN,SAAS;GACT,MAAM;IACJ,aACE;IACF,KAAK;GACP;GACA,UAAU,EACR,WACE,4FACJ;GACA,QAAQ,CAAC;EACX;EACA,OAAO,SAAS;GACd,MAAM,aAAa,QAAQ;GAC3B,OAAO,EACL,wBAAwB,MAAM;IAC5B,IAAI,KAAK,KAAK,SAAS,kBACrB;IAGF,MAAM,aAAa,WAAW,eAAe,KAAK,MAAM,EACtD,SAAS,UAAU,MAAM,SAAS,gBAAgB,MAAM,UAAU,KACpE,CAAC;IACD,IAAI,CAAC,YACH;IAEF,MAAM,iBAAiB,WAAW,cAAc,UAAU;IAG1D,MAAM,YAAY,WAAW,aAAa,IAAI;IAC9C,IAAI,CAAC,kBAAkB,CAAC,WACtB;IAEF,IAAI,WAAW,IAAI,IAAI,SAAS,eAAe,IAAI,MAAM,MACvD;IAKF,MAAM,SAAS,CAAC,WAAW,qBAAqB,YAAY,cAAc;IAE1E,QAAQ,OAAO;KACb,MAAM,KAAK;KACX,WAAW;KACX,KAAK,UACA,UAAU;MACT,MAAM,WAAW,WAAW,KAAK,MAAM,eAAe,MAAM,IAAI,UAAU,MAAM,EAAE;MAClF,OAAO,MAAM,iBACX,CAAC,WAAW,MAAM,IAAI,UAAU,MAAM,EAAE,GACxC,aAAa,SAAS,GACxB;KACF,IACA,KAAA;IACN,CAAC;GACH,EACF;EACF;CCjDiCA,EACjC;AACF"}
package/package.json CHANGED
@@ -1,18 +1,22 @@
1
1
  {
2
2
  "name": "eslint-plugin-ocavue",
3
3
  "type": "module",
4
- "version": "0.0.1",
5
- "description": "TypeScript starter kit",
4
+ "version": "0.1.0",
5
+ "description": "Custom ESLint rules from ocavue",
6
6
  "author": "ocavue <ocavue@gmail.com>",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/ocavue",
9
- "homepage": "https://github.com/ocavue/starter-ts#readme",
9
+ "homepage": "https://github.com/ocavue/eslint-plugin-ocavue#readme",
10
10
  "repository": {
11
11
  "type": "git",
12
- "url": "https://github.com/ocavue/starter-ts.git"
12
+ "url": "https://github.com/ocavue/eslint-plugin-ocavue.git"
13
13
  },
14
- "bugs": "https://github.com/ocavue/starter-ts/issues",
15
- "keywords": [],
14
+ "bugs": "https://github.com/ocavue/eslint-plugin-ocavue/issues",
15
+ "keywords": [
16
+ "eslint",
17
+ "eslintplugin",
18
+ "eslint-plugin"
19
+ ],
16
20
  "sideEffects": false,
17
21
  "main": "./dist/index.js",
18
22
  "module": "./dist/index.js",
@@ -27,6 +31,9 @@
27
31
  "dist",
28
32
  "src"
29
33
  ],
34
+ "peerDependencies": {
35
+ "eslint": "^10.0.0"
36
+ },
30
37
  "devDependencies": {
31
38
  "@ocavue/eslint-config": "^4.13.5",
32
39
  "@ocavue/tsconfig": "^0.7.1",
@@ -36,7 +43,6 @@
36
43
  "oxfmt": "^0.65.0",
37
44
  "tsdown": "^0.22.14",
38
45
  "typescript": "^6.0.3",
39
- "vite": "^8.2.2",
40
46
  "vitest": "^4.1.11"
41
47
  },
42
48
  "scripts": {
package/src/index.ts CHANGED
@@ -1,2 +1,17 @@
1
- export const one = 1
2
- export const two = 2
1
+ import type { ESLint } from 'eslint'
2
+
3
+ import { version } from '../package.json'
4
+
5
+ import noImplicitArrowLinebreak from './rules/no-implicit-arrow-linebreak'
6
+
7
+ const plugin = {
8
+ meta: {
9
+ name: 'eslint-plugin-ocavue',
10
+ version,
11
+ },
12
+ rules: {
13
+ 'no-implicit-arrow-linebreak': noImplicitArrowLinebreak,
14
+ },
15
+ } satisfies ESLint.Plugin
16
+
17
+ export default plugin
@@ -0,0 +1,53 @@
1
+ import { RuleTester } from 'eslint'
2
+ import { test } from 'vitest'
3
+
4
+ import rule from './no-implicit-arrow-linebreak'
5
+
6
+ test('no-implicit-arrow-linebreak', () => {
7
+ const tester = new RuleTester({
8
+ languageOptions: { ecmaVersion: 'latest', sourceType: 'module' },
9
+ })
10
+
11
+ tester.run('no-implicit-arrow-linebreak', rule, {
12
+ valid: [
13
+ 'const f = () => fn(123)',
14
+ 'const f = () => {\n return fn(123)\n}',
15
+ 'const f = () => (\n fn(123)\n)',
16
+ 'const f = async () => fn(123)',
17
+ 'const f = (a) => (b) => a + b',
18
+ ],
19
+ invalid: [
20
+ {
21
+ code: 'const f = () =>\n fn(123)',
22
+ output: 'const f = () => { return fn(123) }',
23
+ errors: [{ messageId: 'linebreak' }],
24
+ },
25
+ {
26
+ code: 'const f = () =>\n (fn(123))',
27
+ output: 'const f = () => { return (fn(123)) }',
28
+ errors: [{ messageId: 'linebreak' }],
29
+ },
30
+ {
31
+ code: 'const f = (a, b) =>\n a + b',
32
+ output: 'const f = (a, b) => { return a + b }',
33
+ errors: [{ messageId: 'linebreak' }],
34
+ },
35
+ {
36
+ code: 'const f = async (a) =>\n await fn(a)',
37
+ output: 'const f = async (a) => { return await fn(a) }',
38
+ errors: [{ messageId: 'linebreak' }],
39
+ },
40
+ {
41
+ code: 'const f = (a) => (b) =>\n a + b',
42
+ output: 'const f = (a) => (b) => { return a + b }',
43
+ errors: [{ messageId: 'linebreak' }],
44
+ },
45
+ {
46
+ // No autofix when a comment sits between `=>` and the body.
47
+ code: 'const f = () => // note\n fn(123)',
48
+ output: null,
49
+ errors: [{ messageId: 'linebreak' }],
50
+ },
51
+ ],
52
+ })
53
+ })
@@ -0,0 +1,65 @@
1
+ import type { Rule } from 'eslint'
2
+
3
+ const rule: Rule.RuleModule = {
4
+ meta: {
5
+ type: 'layout',
6
+ fixable: 'code',
7
+ docs: {
8
+ description:
9
+ 'disallow a linebreak between `=>` and an implicit arrow function body, fixing it to a block body with `return`',
10
+ url: 'https://github.com/ocavue/eslint-plugin-ocavue#no-implicit-arrow-linebreak',
11
+ },
12
+ messages: {
13
+ linebreak:
14
+ 'Implicit arrow body must not start on a new line. Use a block body with `return` instead.',
15
+ },
16
+ schema: [],
17
+ },
18
+ create(context) {
19
+ const sourceCode = context.sourceCode
20
+ return {
21
+ ArrowFunctionExpression(node) {
22
+ if (node.body.type === 'BlockStatement') {
23
+ return
24
+ }
25
+
26
+ const arrowToken = sourceCode.getTokenBefore(node.body, {
27
+ filter: (token) => token.type === 'Punctuator' && token.value === '=>',
28
+ })
29
+ if (!arrowToken) {
30
+ return
31
+ }
32
+ const firstBodyToken = sourceCode.getTokenAfter(arrowToken)
33
+ // The last token of the arrow function. When the body is wrapped in
34
+ // parentheses, this is the closing `)`, so the fix keeps the parens.
35
+ const lastToken = sourceCode.getLastToken(node)
36
+ if (!firstBodyToken || !lastToken) {
37
+ return
38
+ }
39
+ if (arrowToken.loc.end.line === firstBodyToken.loc.start.line) {
40
+ return
41
+ }
42
+
43
+ // The fix rewrites the region between `=>` and the body, so bail out
44
+ // of fixing (but still report) if that region contains comments.
45
+ const canFix = !sourceCode.commentsExistBetween(arrowToken, firstBodyToken)
46
+
47
+ context.report({
48
+ node: node.body,
49
+ messageId: 'linebreak',
50
+ fix: canFix
51
+ ? (fixer) => {
52
+ const bodyText = sourceCode.text.slice(firstBodyToken.range[0], lastToken.range[1])
53
+ return fixer.replaceTextRange(
54
+ [arrowToken.range[1], lastToken.range[1]],
55
+ ` { return ${bodyText} }`,
56
+ )
57
+ }
58
+ : undefined,
59
+ })
60
+ },
61
+ }
62
+ },
63
+ }
64
+
65
+ export default rule
package/src/index.test.ts DELETED
@@ -1,7 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
-
3
- describe('should', () => {
4
- it('exported', () => {
5
- expect(1).toEqual(1)
6
- })
7
- })