@santux/oxc-config 0.0.0 → 1.0.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 +80 -28
- package/package.json +123 -6
- package/src/astro-react.d.ts +4 -0
- package/src/astro-react.js +7 -0
- package/src/astro.d.ts +4 -0
- package/src/astro.js +12 -0
- package/src/commitlint.d.ts +2 -0
- package/src/commitlint.js +8 -0
- package/src/format-astro.d.ts +4 -0
- package/src/format-astro.js +4 -0
- package/src/format-next.d.ts +4 -0
- package/src/format-next.js +6 -0
- package/src/format-tailwind.d.ts +4 -0
- package/src/format-tailwind.js +4 -0
- package/src/format.d.ts +4 -0
- package/src/format.js +18 -0
- package/src/import-sorting.d.ts +5 -0
- package/src/import-sorting.js +25 -0
- package/src/js-plugins.js +10 -0
- package/src/lint.d.ts +4 -0
- package/src/lint.js +238 -0
- package/src/next.d.ts +4 -0
- package/src/next.js +31 -0
- package/src/react-vite.d.ts +4 -0
- package/src/react-vite.js +7 -0
- package/src/react.js +93 -0
- package/src/tailwind.d.ts +4 -0
- package/src/tailwind.js +13 -0
- package/src/tanstack-start.d.ts +4 -0
- package/src/tanstack-start.js +14 -0
package/README.md
CHANGED
|
@@ -1,45 +1,97 @@
|
|
|
1
1
|
# @santux/oxc-config
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Shared oxlint, oxfmt and commitlint config for TypeScript projects. It follows the opinions of [`@santux/eslint-config`](../eslint-config) with native oxlint rules, plus JS plugins for the few rules oxlint has no native version of.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Entries
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
| Entry | Tool | Export |
|
|
8
|
+
| ------------------------------------ | ---------- | ------------- |
|
|
9
|
+
| `@santux/oxc-config/lint` | oxlint | Config object |
|
|
10
|
+
| `@santux/oxc-config/next` | oxlint | Config object |
|
|
11
|
+
| `@santux/oxc-config/react-vite` | oxlint | Config object |
|
|
12
|
+
| `@santux/oxc-config/tanstack-start` | oxlint | Config object |
|
|
13
|
+
| `@santux/oxc-config/astro` | oxlint | Config object |
|
|
14
|
+
| `@santux/oxc-config/astro-react` | oxlint | Config object |
|
|
15
|
+
| `@santux/oxc-config/tailwind` | oxlint | Config object |
|
|
16
|
+
| `@santux/oxc-config/format` | oxfmt | Config object |
|
|
17
|
+
| `@santux/oxc-config/format-next` | oxfmt | Config object |
|
|
18
|
+
| `@santux/oxc-config/format-astro` | oxfmt | Config object |
|
|
19
|
+
| `@santux/oxc-config/format-tailwind` | oxfmt | Config object |
|
|
20
|
+
| `@santux/oxc-config/commitlint` | commitlint | Config object |
|
|
8
21
|
|
|
9
|
-
|
|
22
|
+
The lint entry enables type-aware rules (`options.typeAware`, run by `oxlint-tsgolint`) with the equivalents of typescript-eslint `strictTypeChecked` + `stylisticTypeChecked`, plus `import`, unicorn and `unicorn/filename-case` (kebab-case). Type-aware rules are off in `.js`, `.cjs` and `.mjs` files, as with typescript-eslint `disableTypeChecked`.
|
|
10
23
|
|
|
11
|
-
|
|
12
|
-
1. Configure OIDC trusted publishing for the package name `@santux/oxc-config`
|
|
13
|
-
2. Enable secure, token-less publishing from CI/CD workflows
|
|
14
|
-
3. Establish provenance for packages published under this name
|
|
24
|
+
Rules oxlint has no native version of run as [JS plugins](https://oxc.rs/docs/guide/usage/linter/js-plugins.html), which oxlint marks as alpha. Only rules that need no type information are used:
|
|
15
25
|
|
|
16
|
-
|
|
26
|
+
| Rule id | Source |
|
|
27
|
+
| ---------------------------------------------------------- | ----------------------------------------------- |
|
|
28
|
+
| `check-file/folder-naming-convention` | `eslint-plugin-check-file` |
|
|
29
|
+
| `react-js/jsx-sort-props`, `react-js/jsx-no-leaked-render` | `eslint-plugin-react`, in the Framework entries |
|
|
30
|
+
| `@stylistic/padding-line-between-statements` | `@stylistic/eslint-plugin` |
|
|
31
|
+
| `shadcn/*` | `@shadcn/lint`, in the `tailwind` entry |
|
|
17
32
|
|
|
18
|
-
|
|
33
|
+
The `next`, `react-vite` and `tanstack-start` entries build on the lint entry with react, react-hooks and jsx-a11y rules (`react.version` is `19`). `next` adds the Next.js rules and allows App Router folder names, `react-vite` adds `react/only-export-components`, and `tanstack-start` turns off the file and folder name rules under `routes`. Compose them after the base: `defineConfig({ extends: [santux, next] })`. They need `eslint-plugin-react`, an optional peer dependency.
|
|
19
34
|
|
|
20
|
-
|
|
35
|
+
The `astro` entry adds browser and Node globals and turns off the file and folder name rules under `pages`, plus the file name rule for `.astro` components. oxlint only lints the script blocks of `.astro` files (the frontmatter and `<script>` tags), without type-aware rules, and has no Astro rules or template checks. Set `**/.astro/**` in your own `ignorePatterns`.
|
|
21
36
|
|
|
22
|
-
|
|
37
|
+
The `astro-react` entry adds the React rules of the Framework entries to the `.jsx` and `.tsx` files of an Astro project with React islands (`@astrojs/react`), without `only-export-components`. Compose it after the `astro` entry: `defineConfig({ extends: [santux, astro, astroReact] })`. It needs `eslint-plugin-react`.
|
|
23
38
|
|
|
24
|
-
|
|
25
|
-
2. Configure the trusted publisher (e.g., GitHub Actions)
|
|
26
|
-
3. Specify the repository and workflow that should be allowed to publish
|
|
27
|
-
4. Use the configured workflow to publish your actual package
|
|
39
|
+
The `tailwind` entry adds [`@shadcn/lint`](https://github.com/shadcn-ui/lint) (an optional peer dependency) with every rule except `require-static-classes`: `no-unknown-classes`, `no-raw-colors`, `no-arbitrary-values`, `no-inline-styles` and `no-restyle`. It needs no shadcn/ui: it finds the stylesheet that imports Tailwind, and it reads `components.json` when there is one. `no-restyle` only knows your design system components through `components.json` or `settings.shadcn.componentImports`.
|
|
28
40
|
|
|
29
|
-
|
|
41
|
+
The entries set no `ignorePatterns`, because oxlint does not inherit them through `extends`. oxlint already skips what `.gitignore` lists; set `ignorePatterns` in your own `oxlint.config.ts` for anything else.
|
|
30
42
|
|
|
31
|
-
|
|
32
|
-
- Contains no executable code
|
|
33
|
-
- Provides no functionality
|
|
34
|
-
- Should not be installed as a dependency
|
|
35
|
-
- Exists only for administrative purposes
|
|
43
|
+
oxfmt has no `extends`, so the format entries are plain objects that you import and spread. The `format` entry uses the same Prettier options as the `eslint` package, with `endOfLine: 'lf'` (oxfmt doesn't support `auto`), and sorts imports to reproduce the `eslint` import order: `type`, `react`, `builtin`, `external`, `internal`, `alias` (`@/**`), `parent`, `sibling`, `index`, with a blank line between groups, case-insensitive ascending order, and side-effect imports sorted too. Outside Next.js the `react` group is `react`, `react-dom` and `react-dom/**`. In a Next.js project spread `format-next` after `format` to add `next` and `next/**` to that group. The `format-tailwind` entry enables oxfmt Tailwind class sorting, so no plugin is needed. The `format-astro` entry turns on `.astro` formatting, which oxfmt leaves off by default. It needs `@astrojs/compiler-rs`, an optional peer dependency that oxfmt does not bundle.
|
|
36
44
|
|
|
37
|
-
##
|
|
45
|
+
## Usage
|
|
38
46
|
|
|
39
|
-
|
|
40
|
-
- [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
|
|
41
|
-
- [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
|
|
47
|
+
Every tool is an exact-version peer dependency and must be installed in your project:
|
|
42
48
|
|
|
43
|
-
|
|
49
|
+
```sh
|
|
50
|
+
pnpm add -D @santux/oxc-config @commitlint/cli @commitlint/config-conventional @stylistic/eslint-plugin eslint-plugin-check-file husky oxfmt oxlint oxlint-tsgolint
|
|
51
|
+
```
|
|
44
52
|
|
|
45
|
-
|
|
53
|
+
```ts
|
|
54
|
+
// oxlint.config.ts
|
|
55
|
+
import santux from '@santux/oxc-config/lint'
|
|
56
|
+
import next from '@santux/oxc-config/next'
|
|
57
|
+
import { defineConfig } from 'oxlint'
|
|
58
|
+
|
|
59
|
+
export default defineConfig({
|
|
60
|
+
extends: [santux, next],
|
|
61
|
+
// Override, disable or add rules locally
|
|
62
|
+
rules: { 'no-console': 'off' },
|
|
63
|
+
})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// oxfmt.config.ts
|
|
68
|
+
import santux from '@santux/oxc-config/format'
|
|
69
|
+
import formatNext from '@santux/oxc-config/format-next'
|
|
70
|
+
import formatTailwind from '@santux/oxc-config/format-tailwind'
|
|
71
|
+
import { defineConfig } from 'oxfmt'
|
|
72
|
+
|
|
73
|
+
export default defineConfig({ ...santux, ...formatNext, ...formatTailwind, printWidth: 120 })
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
// commitlint.config.js
|
|
78
|
+
export { default } from '@santux/oxc-config/commitlint'
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Requires Node `>=22.18`, which oxlint needs to auto-discover TypeScript config files.
|
|
82
|
+
|
|
83
|
+
## Differences from `@santux/eslint-config`
|
|
84
|
+
|
|
85
|
+
The `eslint` package is the reference. The differences left are:
|
|
86
|
+
|
|
87
|
+
- File names are checked with `unicorn/filename-case` instead of check-file.
|
|
88
|
+
- react-hooks `config` and `gating` are dropped. They validate React Compiler options, not your code.
|
|
89
|
+
- `import/no-extraneous-dependencies` and `import/no-useless-path-segments` are dropped. oxlint has no native version, and `eslint-plugin-import-x` loads ESLint at runtime.
|
|
90
|
+
|
|
91
|
+
`import/order` is replaced by oxfmt import sorting. ESLint's `import/order` does not move imports across side-effect imports, while oxfmt with side-effect sorting does, so files with CSS or `server-only` imports can end up ordered differently between the two packages.
|
|
92
|
+
|
|
93
|
+
`import/no-cycle` was verified on a sample project: it detects cycles through `.ts` files and `@/` imports, so it stays enabled.
|
|
94
|
+
|
|
95
|
+
## Versioning
|
|
96
|
+
|
|
97
|
+
This package follows strict semver: **any change that can make previously passing code fail is a major release**. That includes a new error-level rule, a rule moving from warning to error, and a newer required tool or Node version. Projects can stay on an older major for as long as they want.
|
package/package.json
CHANGED
|
@@ -1,10 +1,127 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@santux/oxc-config",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Shared oxlint, oxfmt and commitlint config for TypeScript projects.",
|
|
5
5
|
"keywords": [
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
6
|
+
"commitlint",
|
|
7
|
+
"oxc",
|
|
8
|
+
"oxfmt",
|
|
9
|
+
"oxlint"
|
|
10
|
+
],
|
|
11
|
+
"license": "ISC",
|
|
12
|
+
"author": {
|
|
13
|
+
"name": "Nicolás Santuccio",
|
|
14
|
+
"email": "nico.santuccio@gmail.com"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/nicosantux/santux.git",
|
|
19
|
+
"directory": "packages/oxc-config"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"src",
|
|
23
|
+
"!src/test",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"exports": {
|
|
28
|
+
"./lint": {
|
|
29
|
+
"types": "./src/lint.d.ts",
|
|
30
|
+
"default": "./src/lint.js"
|
|
31
|
+
},
|
|
32
|
+
"./format": {
|
|
33
|
+
"types": "./src/format.d.ts",
|
|
34
|
+
"default": "./src/format.js"
|
|
35
|
+
},
|
|
36
|
+
"./format-astro": {
|
|
37
|
+
"types": "./src/format-astro.d.ts",
|
|
38
|
+
"default": "./src/format-astro.js"
|
|
39
|
+
},
|
|
40
|
+
"./format-next": {
|
|
41
|
+
"types": "./src/format-next.d.ts",
|
|
42
|
+
"default": "./src/format-next.js"
|
|
43
|
+
},
|
|
44
|
+
"./format-tailwind": {
|
|
45
|
+
"types": "./src/format-tailwind.d.ts",
|
|
46
|
+
"default": "./src/format-tailwind.js"
|
|
47
|
+
},
|
|
48
|
+
"./commitlint": {
|
|
49
|
+
"types": "./src/commitlint.d.ts",
|
|
50
|
+
"default": "./src/commitlint.js"
|
|
51
|
+
},
|
|
52
|
+
"./astro": {
|
|
53
|
+
"types": "./src/astro.d.ts",
|
|
54
|
+
"default": "./src/astro.js"
|
|
55
|
+
},
|
|
56
|
+
"./astro-react": {
|
|
57
|
+
"types": "./src/astro-react.d.ts",
|
|
58
|
+
"default": "./src/astro-react.js"
|
|
59
|
+
},
|
|
60
|
+
"./next": {
|
|
61
|
+
"types": "./src/next.d.ts",
|
|
62
|
+
"default": "./src/next.js"
|
|
63
|
+
},
|
|
64
|
+
"./react-vite": {
|
|
65
|
+
"types": "./src/react-vite.d.ts",
|
|
66
|
+
"default": "./src/react-vite.js"
|
|
67
|
+
},
|
|
68
|
+
"./tanstack-start": {
|
|
69
|
+
"types": "./src/tanstack-start.d.ts",
|
|
70
|
+
"default": "./src/tanstack-start.js"
|
|
71
|
+
},
|
|
72
|
+
"./tailwind": {
|
|
73
|
+
"types": "./src/tailwind.d.ts",
|
|
74
|
+
"default": "./src/tailwind.js"
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"publishConfig": {
|
|
78
|
+
"access": "public"
|
|
79
|
+
},
|
|
80
|
+
"scripts": {
|
|
81
|
+
"test": "vitest run",
|
|
82
|
+
"typecheck": "tsc --noEmit"
|
|
83
|
+
},
|
|
84
|
+
"devDependencies": {
|
|
85
|
+
"@astrojs/compiler-rs": "0.5.3",
|
|
86
|
+
"@commitlint/cli": "21.2.4",
|
|
87
|
+
"@commitlint/config-conventional": "21.2.4",
|
|
88
|
+
"@shadcn/lint": "0.2.5",
|
|
89
|
+
"@stylistic/eslint-plugin": "5.10.0",
|
|
90
|
+
"eslint-plugin-check-file": "3.3.2",
|
|
91
|
+
"eslint-plugin-react": "7.37.5",
|
|
92
|
+
"husky": "9.1.7",
|
|
93
|
+
"oxfmt": "0.78.0",
|
|
94
|
+
"oxlint": "1.93.0",
|
|
95
|
+
"oxlint-tsgolint": "7.0.2007",
|
|
96
|
+
"tailwindcss": "4.3.5",
|
|
97
|
+
"typescript": "6.0.3",
|
|
98
|
+
"vitest": "5.1.0"
|
|
99
|
+
},
|
|
100
|
+
"peerDependencies": {
|
|
101
|
+
"@astrojs/compiler-rs": "0.5.3",
|
|
102
|
+
"@commitlint/cli": "21.2.4",
|
|
103
|
+
"@commitlint/config-conventional": "21.2.4",
|
|
104
|
+
"@shadcn/lint": "0.2.5",
|
|
105
|
+
"@stylistic/eslint-plugin": "5.10.0",
|
|
106
|
+
"eslint-plugin-check-file": "3.3.2",
|
|
107
|
+
"eslint-plugin-react": "7.37.5",
|
|
108
|
+
"husky": "9.1.7",
|
|
109
|
+
"oxfmt": "0.78.0",
|
|
110
|
+
"oxlint": "1.93.0",
|
|
111
|
+
"oxlint-tsgolint": "7.0.2007"
|
|
112
|
+
},
|
|
113
|
+
"peerDependenciesMeta": {
|
|
114
|
+
"@astrojs/compiler-rs": {
|
|
115
|
+
"optional": true
|
|
116
|
+
},
|
|
117
|
+
"@shadcn/lint": {
|
|
118
|
+
"optional": true
|
|
119
|
+
},
|
|
120
|
+
"eslint-plugin-react": {
|
|
121
|
+
"optional": true
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
"engines": {
|
|
125
|
+
"node": ">=22.18"
|
|
126
|
+
}
|
|
10
127
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { reactConfig } from './react.js'
|
|
2
|
+
|
|
3
|
+
// oxlint `overrides` do not accept `settings`
|
|
4
|
+
const { settings, ...config } = reactConfig({ env: { browser: true } })
|
|
5
|
+
|
|
6
|
+
/** @type {import('oxlint').OxlintConfig} */
|
|
7
|
+
export default { settings, overrides: [{ files: ['**/*.{jsx,tsx}'], ...config }] }
|
package/src/astro.d.ts
ADDED
package/src/astro.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** @type {import('oxlint').OxlintConfig} */
|
|
2
|
+
export default {
|
|
3
|
+
env: { browser: true, node: true },
|
|
4
|
+
overrides: [
|
|
5
|
+
{
|
|
6
|
+
files: ['**/pages/**'],
|
|
7
|
+
rules: { 'check-file/folder-naming-convention': 'off', 'unicorn/filename-case': 'off' },
|
|
8
|
+
},
|
|
9
|
+
// Astro components are PascalCase, and the eslint package does not check .astro file names
|
|
10
|
+
{ files: ['**/*.astro'], rules: { 'unicorn/filename-case': 'off' } },
|
|
11
|
+
],
|
|
12
|
+
}
|
package/src/format.d.ts
ADDED
package/src/format.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { importSorting } from './import-sorting.js'
|
|
2
|
+
|
|
3
|
+
/** @type {import('oxfmt').OxfmtConfig} */
|
|
4
|
+
export default {
|
|
5
|
+
arrowParens: 'always',
|
|
6
|
+
bracketSameLine: false,
|
|
7
|
+
bracketSpacing: true,
|
|
8
|
+
endOfLine: 'lf',
|
|
9
|
+
jsxSingleQuote: true,
|
|
10
|
+
printWidth: 100,
|
|
11
|
+
quoteProps: 'as-needed',
|
|
12
|
+
semi: false,
|
|
13
|
+
singleQuote: true,
|
|
14
|
+
sortImports: importSorting(['react', 'react-dom', 'react-dom/**']),
|
|
15
|
+
tabWidth: 2,
|
|
16
|
+
trailingComma: 'all',
|
|
17
|
+
useTabs: false,
|
|
18
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** oxfmt `sortImports` options. Packages in `reactPackages` are grouped before the other builtins. */
|
|
2
|
+
export function importSorting(reactPackages) {
|
|
3
|
+
return {
|
|
4
|
+
customGroups: [
|
|
5
|
+
{ groupName: 'react', modifiers: ['value'], elementNamePattern: reactPackages },
|
|
6
|
+
{ groupName: 'alias', modifiers: ['value'], elementNamePattern: ['@/**'] },
|
|
7
|
+
],
|
|
8
|
+
groups: [
|
|
9
|
+
'type',
|
|
10
|
+
'react',
|
|
11
|
+
'builtin',
|
|
12
|
+
'external',
|
|
13
|
+
'internal',
|
|
14
|
+
'alias',
|
|
15
|
+
'parent',
|
|
16
|
+
'sibling',
|
|
17
|
+
'index',
|
|
18
|
+
'unknown',
|
|
19
|
+
],
|
|
20
|
+
ignoreCase: true,
|
|
21
|
+
newlinesBetween: true,
|
|
22
|
+
order: 'asc',
|
|
23
|
+
sortSideEffects: true,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* An oxlint `jsPlugins` item resolved from this package, so the plugin loads from wherever the
|
|
5
|
+
* package manager installed this package's peers.
|
|
6
|
+
*/
|
|
7
|
+
export const jsPlugin = (name, specifier) => ({
|
|
8
|
+
name,
|
|
9
|
+
specifier: fileURLToPath(import.meta.resolve(specifier)),
|
|
10
|
+
})
|
package/src/lint.d.ts
ADDED
package/src/lint.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { jsPlugin } from './js-plugins.js'
|
|
2
|
+
|
|
3
|
+
/** @type {import('oxlint').OxlintConfig} */
|
|
4
|
+
export default {
|
|
5
|
+
plugins: ['import', 'oxc', 'typescript', 'unicorn'],
|
|
6
|
+
jsPlugins: [
|
|
7
|
+
jsPlugin('check-file', 'eslint-plugin-check-file'),
|
|
8
|
+
jsPlugin('@stylistic', '@stylistic/eslint-plugin'),
|
|
9
|
+
],
|
|
10
|
+
categories: { correctness: 'error' },
|
|
11
|
+
options: { typeAware: true },
|
|
12
|
+
rules: {
|
|
13
|
+
// Vanilla
|
|
14
|
+
eqeqeq: ['error', 'always', { null: 'ignore' }],
|
|
15
|
+
'no-await-in-loop': 'error',
|
|
16
|
+
'no-case-declarations': 'error',
|
|
17
|
+
'no-console': 'error',
|
|
18
|
+
'no-else-return': 'warn',
|
|
19
|
+
'no-empty': 'error',
|
|
20
|
+
'no-fallthrough': 'error',
|
|
21
|
+
'no-implicit-coercion': 'error',
|
|
22
|
+
'no-param-reassign': 'error',
|
|
23
|
+
'no-prototype-builtins': 'error',
|
|
24
|
+
'no-regex-spaces': 'error',
|
|
25
|
+
'no-self-compare': 'error',
|
|
26
|
+
'no-unneeded-ternary': 'error',
|
|
27
|
+
'no-var': 'error',
|
|
28
|
+
'object-shorthand': 'warn',
|
|
29
|
+
'prefer-const': 'warn',
|
|
30
|
+
'prefer-object-spread': 'warn',
|
|
31
|
+
'prefer-rest-params': 'error',
|
|
32
|
+
'prefer-spread': 'error',
|
|
33
|
+
'prefer-template': 'warn',
|
|
34
|
+
// File naming
|
|
35
|
+
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
|
36
|
+
'check-file/folder-naming-convention': ['error', { '**/src/**/': 'KEBAB_CASE' }],
|
|
37
|
+
// Imports
|
|
38
|
+
'import/first': 'error',
|
|
39
|
+
'import/newline-after-import': 'warn',
|
|
40
|
+
'import/no-duplicates': ['warn', { preferInline: true }],
|
|
41
|
+
'import/no-cycle': 'error',
|
|
42
|
+
'import/no-mutable-exports': 'error',
|
|
43
|
+
'import/no-self-import': 'error',
|
|
44
|
+
// Stylistic
|
|
45
|
+
'@stylistic/padding-line-between-statements': [
|
|
46
|
+
'warn',
|
|
47
|
+
{ blankLine: 'always', prev: 'directive', next: '*' },
|
|
48
|
+
{ blankLine: 'always', prev: '*', next: 'block-like' },
|
|
49
|
+
{ blankLine: 'always', prev: 'block-like', next: '*' },
|
|
50
|
+
{ blankLine: 'always', prev: '*', next: 'return' },
|
|
51
|
+
{ blankLine: 'always', prev: ['const', 'let', 'var'], next: '*' },
|
|
52
|
+
{
|
|
53
|
+
blankLine: 'any',
|
|
54
|
+
prev: ['singleline-const', 'singleline-let', 'singleline-var'],
|
|
55
|
+
next: ['singleline-const', 'singleline-let', 'singleline-var'],
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
// typescript-eslint strictTypeChecked + stylisticTypeChecked
|
|
59
|
+
'typescript/adjacent-overload-signatures': 'error',
|
|
60
|
+
'typescript/array-type': 'error',
|
|
61
|
+
'typescript/await-thenable': 'error',
|
|
62
|
+
'typescript/ban-ts-comment': 'error',
|
|
63
|
+
'typescript/ban-tslint-comment': 'error',
|
|
64
|
+
'typescript/class-literal-property-style': 'error',
|
|
65
|
+
'typescript/consistent-generic-constructors': 'error',
|
|
66
|
+
'typescript/consistent-indexed-object-style': 'error',
|
|
67
|
+
'typescript/consistent-type-assertions': 'error',
|
|
68
|
+
'typescript/consistent-type-definitions': 'error',
|
|
69
|
+
'typescript/consistent-type-exports': 'warn',
|
|
70
|
+
'typescript/consistent-type-imports': ['warn', { fixStyle: 'inline-type-imports' }],
|
|
71
|
+
'typescript/dot-notation': 'error',
|
|
72
|
+
'typescript/method-signature-style': 'warn',
|
|
73
|
+
'typescript/no-array-delete': 'error',
|
|
74
|
+
'typescript/no-base-to-string': 'error',
|
|
75
|
+
'typescript/no-confusing-non-null-assertion': 'error',
|
|
76
|
+
'typescript/no-confusing-void-expression': ['error', { ignoreArrowShorthand: true }],
|
|
77
|
+
'typescript/no-deprecated': 'error',
|
|
78
|
+
'typescript/no-duplicate-enum-values': 'error',
|
|
79
|
+
'typescript/no-duplicate-type-constituents': 'error',
|
|
80
|
+
'typescript/no-dynamic-delete': 'error',
|
|
81
|
+
'typescript/no-empty-object-type': 'error',
|
|
82
|
+
'typescript/no-explicit-any': 'error',
|
|
83
|
+
'typescript/no-extra-non-null-assertion': 'error',
|
|
84
|
+
'typescript/no-extraneous-class': 'error',
|
|
85
|
+
'typescript/no-floating-promises': 'error',
|
|
86
|
+
'typescript/no-for-in-array': 'error',
|
|
87
|
+
'typescript/no-import-type-side-effects': 'warn',
|
|
88
|
+
'typescript/no-implied-eval': 'error',
|
|
89
|
+
'typescript/no-inferrable-types': 'error',
|
|
90
|
+
'typescript/no-invalid-void-type': 'error',
|
|
91
|
+
'typescript/no-meaningless-void-operator': 'error',
|
|
92
|
+
'typescript/no-misused-new': 'error',
|
|
93
|
+
'typescript/no-misused-promises': 'error',
|
|
94
|
+
'typescript/no-mixed-enums': 'error',
|
|
95
|
+
'typescript/no-namespace': 'error',
|
|
96
|
+
'typescript/no-non-null-asserted-nullish-coalescing': 'error',
|
|
97
|
+
'typescript/no-non-null-asserted-optional-chain': 'error',
|
|
98
|
+
'typescript/no-non-null-assertion': 'error',
|
|
99
|
+
'typescript/no-redundant-type-constituents': 'warn',
|
|
100
|
+
'typescript/no-require-imports': 'error',
|
|
101
|
+
'typescript/no-this-alias': 'error',
|
|
102
|
+
'typescript/no-unnecessary-boolean-literal-compare': 'error',
|
|
103
|
+
'typescript/no-unnecessary-condition': 'error',
|
|
104
|
+
'typescript/no-unnecessary-qualifier': 'warn',
|
|
105
|
+
'typescript/no-unnecessary-template-expression': 'error',
|
|
106
|
+
'typescript/no-unnecessary-type-arguments': 'error',
|
|
107
|
+
'typescript/no-unnecessary-type-assertion': 'error',
|
|
108
|
+
'typescript/no-unnecessary-type-constraint': 'error',
|
|
109
|
+
'typescript/no-unnecessary-type-conversion': 'error',
|
|
110
|
+
'typescript/no-unnecessary-type-parameters': 'error',
|
|
111
|
+
'typescript/no-unsafe-argument': 'error',
|
|
112
|
+
'typescript/no-unsafe-assignment': 'error',
|
|
113
|
+
'typescript/no-unsafe-call': 'error',
|
|
114
|
+
'typescript/no-unsafe-declaration-merging': 'error',
|
|
115
|
+
'typescript/no-unsafe-enum-comparison': 'error',
|
|
116
|
+
'typescript/no-unsafe-function-type': 'error',
|
|
117
|
+
'typescript/no-unsafe-member-access': 'error',
|
|
118
|
+
'typescript/no-unsafe-return': 'error',
|
|
119
|
+
'typescript/no-unsafe-unary-minus': 'error',
|
|
120
|
+
'typescript/no-wrapper-object-types': 'error',
|
|
121
|
+
'typescript/non-nullable-type-assertion-style': 'error',
|
|
122
|
+
'typescript/only-throw-error': 'error',
|
|
123
|
+
'typescript/prefer-as-const': 'error',
|
|
124
|
+
'typescript/prefer-find': 'error',
|
|
125
|
+
'typescript/prefer-for-of': 'error',
|
|
126
|
+
'typescript/prefer-function-type': 'error',
|
|
127
|
+
'typescript/prefer-includes': 'error',
|
|
128
|
+
'typescript/prefer-literal-enum-member': 'error',
|
|
129
|
+
'typescript/prefer-namespace-keyword': 'error',
|
|
130
|
+
'typescript/prefer-nullish-coalescing': [
|
|
131
|
+
'error',
|
|
132
|
+
{ ignorePrimitives: { bigint: true, boolean: true, number: true, string: true } },
|
|
133
|
+
],
|
|
134
|
+
'typescript/prefer-optional-chain': 'error',
|
|
135
|
+
'typescript/prefer-promise-reject-errors': 'error',
|
|
136
|
+
'typescript/prefer-reduce-type-parameter': 'error',
|
|
137
|
+
'typescript/prefer-regexp-exec': 'error',
|
|
138
|
+
'typescript/prefer-return-this-type': 'error',
|
|
139
|
+
'typescript/prefer-string-starts-ends-with': 'error',
|
|
140
|
+
'typescript/related-getter-setter-pairs': 'error',
|
|
141
|
+
'typescript/require-array-sort-compare': 'error',
|
|
142
|
+
'typescript/require-await': 'error',
|
|
143
|
+
'typescript/restrict-plus-operands': 'error',
|
|
144
|
+
'typescript/return-await': ['error', 'error-handling-correctness-only'],
|
|
145
|
+
'typescript/restrict-template-expressions': 'error',
|
|
146
|
+
'typescript/switch-exhaustiveness-check': 'error',
|
|
147
|
+
'typescript/triple-slash-reference': 'off',
|
|
148
|
+
'typescript/unbound-method': 'error',
|
|
149
|
+
'typescript/unified-signatures': 'error',
|
|
150
|
+
'typescript/use-unknown-in-catch-callback-variable': 'error',
|
|
151
|
+
'default-param-last': 'error',
|
|
152
|
+
'no-array-constructor': 'error',
|
|
153
|
+
'no-empty-function': 'error',
|
|
154
|
+
'no-loop-func': 'error',
|
|
155
|
+
'no-unused-expressions': 'error',
|
|
156
|
+
'no-unused-vars': [
|
|
157
|
+
'error',
|
|
158
|
+
{
|
|
159
|
+
args: 'all',
|
|
160
|
+
argsIgnorePattern: '^_',
|
|
161
|
+
caughtErrors: 'all',
|
|
162
|
+
caughtErrorsIgnorePattern: '^_',
|
|
163
|
+
destructuredArrayIgnorePattern: '^_',
|
|
164
|
+
varsIgnorePattern: '^_',
|
|
165
|
+
ignoreRestSiblings: true,
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
'no-useless-constructor': 'error',
|
|
169
|
+
// Unicorn
|
|
170
|
+
'unicorn/error-message': 'error',
|
|
171
|
+
'unicorn/no-empty-file': 'error',
|
|
172
|
+
'unicorn/no-instanceof-builtins': 'error',
|
|
173
|
+
'unicorn/no-useless-spread': 'warn',
|
|
174
|
+
'unicorn/prefer-array-flat-map': 'warn',
|
|
175
|
+
'unicorn/prefer-node-protocol': 'warn',
|
|
176
|
+
'unicorn/throw-new-error': 'error',
|
|
177
|
+
},
|
|
178
|
+
overrides: [
|
|
179
|
+
{
|
|
180
|
+
// JS files carry no types, so type-aware rules only report `any` noise, as typescript-eslint `disableTypeChecked` does
|
|
181
|
+
files: ['**/*.{js,cjs,mjs}'],
|
|
182
|
+
rules: {
|
|
183
|
+
'typescript/await-thenable': 'off',
|
|
184
|
+
'typescript/consistent-type-exports': 'off',
|
|
185
|
+
'typescript/dot-notation': 'off',
|
|
186
|
+
'typescript/no-array-delete': 'off',
|
|
187
|
+
'typescript/no-base-to-string': 'off',
|
|
188
|
+
'typescript/no-confusing-void-expression': 'off',
|
|
189
|
+
'typescript/no-deprecated': 'off',
|
|
190
|
+
'typescript/no-duplicate-type-constituents': 'off',
|
|
191
|
+
'typescript/no-floating-promises': 'off',
|
|
192
|
+
'typescript/no-for-in-array': 'off',
|
|
193
|
+
'typescript/no-implied-eval': 'off',
|
|
194
|
+
'typescript/no-meaningless-void-operator': 'off',
|
|
195
|
+
'typescript/no-misused-promises': 'off',
|
|
196
|
+
'typescript/no-misused-spread': 'off',
|
|
197
|
+
'typescript/no-mixed-enums': 'off',
|
|
198
|
+
'typescript/no-redundant-type-constituents': 'off',
|
|
199
|
+
'typescript/no-unnecessary-boolean-literal-compare': 'off',
|
|
200
|
+
'typescript/no-unnecessary-condition': 'off',
|
|
201
|
+
'typescript/no-unnecessary-qualifier': 'off',
|
|
202
|
+
'typescript/no-unnecessary-template-expression': 'off',
|
|
203
|
+
'typescript/no-unnecessary-type-arguments': 'off',
|
|
204
|
+
'typescript/no-unnecessary-type-assertion': 'off',
|
|
205
|
+
'typescript/no-unnecessary-type-conversion': 'off',
|
|
206
|
+
'typescript/no-unnecessary-type-parameters': 'off',
|
|
207
|
+
'typescript/no-unsafe-argument': 'off',
|
|
208
|
+
'typescript/no-unsafe-assignment': 'off',
|
|
209
|
+
'typescript/no-unsafe-call': 'off',
|
|
210
|
+
'typescript/no-unsafe-enum-comparison': 'off',
|
|
211
|
+
'typescript/no-unsafe-member-access': 'off',
|
|
212
|
+
'typescript/no-unsafe-return': 'off',
|
|
213
|
+
'typescript/no-unsafe-unary-minus': 'off',
|
|
214
|
+
'typescript/no-useless-default-assignment': 'off',
|
|
215
|
+
'typescript/non-nullable-type-assertion-style': 'off',
|
|
216
|
+
'typescript/only-throw-error': 'off',
|
|
217
|
+
'typescript/prefer-find': 'off',
|
|
218
|
+
'typescript/prefer-includes': 'off',
|
|
219
|
+
'typescript/prefer-nullish-coalescing': 'off',
|
|
220
|
+
'typescript/prefer-optional-chain': 'off',
|
|
221
|
+
'typescript/prefer-promise-reject-errors': 'off',
|
|
222
|
+
'typescript/prefer-reduce-type-parameter': 'off',
|
|
223
|
+
'typescript/prefer-regexp-exec': 'off',
|
|
224
|
+
'typescript/prefer-return-this-type': 'off',
|
|
225
|
+
'typescript/prefer-string-starts-ends-with': 'off',
|
|
226
|
+
'typescript/related-getter-setter-pairs': 'off',
|
|
227
|
+
'typescript/require-array-sort-compare': 'off',
|
|
228
|
+
'typescript/require-await': 'off',
|
|
229
|
+
'typescript/restrict-plus-operands': 'off',
|
|
230
|
+
'typescript/restrict-template-expressions': 'off',
|
|
231
|
+
'typescript/return-await': 'off',
|
|
232
|
+
'typescript/switch-exhaustiveness-check': 'off',
|
|
233
|
+
'typescript/unbound-method': 'off',
|
|
234
|
+
'typescript/use-unknown-in-catch-callback-variable': 'off',
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
}
|
package/src/next.d.ts
ADDED
package/src/next.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { reactConfig } from './react.js'
|
|
2
|
+
|
|
3
|
+
/** @type {import('oxlint').OxlintConfig} */
|
|
4
|
+
export default reactConfig({
|
|
5
|
+
env: { browser: true, node: true },
|
|
6
|
+
plugins: ['nextjs'],
|
|
7
|
+
rules: {
|
|
8
|
+
'check-file/folder-naming-convention': ['error', { '**/src/**/': 'NEXT_JS_APP_ROUTER_CASE' }],
|
|
9
|
+
'nextjs/google-font-display': 'warn',
|
|
10
|
+
'nextjs/google-font-preconnect': 'warn',
|
|
11
|
+
'nextjs/inline-script-id': 'error',
|
|
12
|
+
'nextjs/next-script-for-ga': 'warn',
|
|
13
|
+
'nextjs/no-assign-module-variable': 'error',
|
|
14
|
+
'nextjs/no-async-client-component': 'warn',
|
|
15
|
+
'nextjs/no-before-interactive-script-outside-document': 'warn',
|
|
16
|
+
'nextjs/no-css-tags': 'warn',
|
|
17
|
+
'nextjs/no-document-import-in-page': 'error',
|
|
18
|
+
'nextjs/no-duplicate-head': 'error',
|
|
19
|
+
'nextjs/no-head-element': 'warn',
|
|
20
|
+
'nextjs/no-head-import-in-document': 'error',
|
|
21
|
+
'nextjs/no-html-link-for-pages': 'error',
|
|
22
|
+
'nextjs/no-img-element': 'warn',
|
|
23
|
+
'nextjs/no-page-custom-font': 'warn',
|
|
24
|
+
'nextjs/no-script-component-in-head': 'error',
|
|
25
|
+
'nextjs/no-styled-jsx-in-document': 'warn',
|
|
26
|
+
'nextjs/no-sync-scripts': 'error',
|
|
27
|
+
'nextjs/no-title-in-document-head': 'warn',
|
|
28
|
+
'nextjs/no-typos': 'warn',
|
|
29
|
+
'nextjs/no-unwanted-polyfillio': 'warn',
|
|
30
|
+
},
|
|
31
|
+
})
|
package/src/react.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { jsPlugin } from './js-plugins.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* React config shared by the Framework entries. Rules from `eslint-plugin-react`,
|
|
5
|
+
* `eslint-plugin-react-hooks` and `eslint-plugin-jsx-a11y` are native in oxlint, except the
|
|
6
|
+
* `react-js` ones, which load `eslint-plugin-react` as a JS plugin.
|
|
7
|
+
*/
|
|
8
|
+
export function reactConfig({ env, plugins = [], rules = {} }) {
|
|
9
|
+
return {
|
|
10
|
+
plugins: ['jsx-a11y', 'react', ...plugins],
|
|
11
|
+
jsPlugins: [jsPlugin('react-js', 'eslint-plugin-react')],
|
|
12
|
+
env,
|
|
13
|
+
settings: { react: { version: '19' } },
|
|
14
|
+
rules: {
|
|
15
|
+
// react
|
|
16
|
+
'react/display-name': 'error',
|
|
17
|
+
'react/function-component-definition': 'warn',
|
|
18
|
+
'react/hook-use-state': 'error',
|
|
19
|
+
'react/jsx-boolean-value': 'warn',
|
|
20
|
+
'react/jsx-curly-brace-presence': 'warn',
|
|
21
|
+
'react/jsx-fragments': 'warn',
|
|
22
|
+
'react/jsx-key': 'error',
|
|
23
|
+
'react/jsx-no-comment-textnodes': 'error',
|
|
24
|
+
'react/jsx-no-duplicate-props': 'error',
|
|
25
|
+
'react/jsx-no-target-blank': ['error', { allowReferrer: true }],
|
|
26
|
+
'react/jsx-no-undef': 'error',
|
|
27
|
+
'react/jsx-no-useless-fragment': ['warn', { allowExpressions: true }],
|
|
28
|
+
'react/no-array-index-key': 'warn',
|
|
29
|
+
'react/no-children-prop': 'error',
|
|
30
|
+
'react/no-danger-with-children': 'error',
|
|
31
|
+
'react/no-direct-mutation-state': 'error',
|
|
32
|
+
'react/no-find-dom-node': 'error',
|
|
33
|
+
'react/no-is-mounted': 'error',
|
|
34
|
+
'react/no-render-return-value': 'error',
|
|
35
|
+
'react/no-string-refs': 'error',
|
|
36
|
+
'react/no-unescaped-entities': 'error',
|
|
37
|
+
'react/no-unknown-property': 'error',
|
|
38
|
+
'react/no-unstable-nested-components': 'error',
|
|
39
|
+
'react/require-render-return': 'error',
|
|
40
|
+
'react/self-closing-comp': 'warn',
|
|
41
|
+
'react-js/jsx-no-leaked-render': 'warn',
|
|
42
|
+
'react-js/jsx-sort-props': 'warn',
|
|
43
|
+
// react-hooks
|
|
44
|
+
'react/error-boundaries': 'error',
|
|
45
|
+
'react/exhaustive-deps': 'warn',
|
|
46
|
+
'react/globals': 'error',
|
|
47
|
+
'react/immutability': 'error',
|
|
48
|
+
'react/incompatible-library': 'warn',
|
|
49
|
+
'react/preserve-manual-memoization': 'error',
|
|
50
|
+
'react/purity': 'error',
|
|
51
|
+
'react/refs': 'error',
|
|
52
|
+
'react/rules-of-hooks': 'error',
|
|
53
|
+
'react/set-state-in-effect': 'error',
|
|
54
|
+
'react/set-state-in-render': 'error',
|
|
55
|
+
'react/static-components': 'error',
|
|
56
|
+
'react/unsupported-syntax': 'warn',
|
|
57
|
+
'react/use-memo': 'error',
|
|
58
|
+
// jsx-a11y
|
|
59
|
+
'jsx-a11y/alt-text': 'error',
|
|
60
|
+
'jsx-a11y/anchor-has-content': 'error',
|
|
61
|
+
'jsx-a11y/anchor-is-valid': 'error',
|
|
62
|
+
'jsx-a11y/aria-activedescendant-has-tabindex': 'error',
|
|
63
|
+
'jsx-a11y/aria-props': 'error',
|
|
64
|
+
'jsx-a11y/aria-proptypes': 'error',
|
|
65
|
+
'jsx-a11y/aria-role': 'error',
|
|
66
|
+
'jsx-a11y/aria-unsupported-elements': 'error',
|
|
67
|
+
'jsx-a11y/autocomplete-valid': 'error',
|
|
68
|
+
'jsx-a11y/click-events-have-key-events': 'error',
|
|
69
|
+
'jsx-a11y/heading-has-content': 'error',
|
|
70
|
+
'jsx-a11y/html-has-lang': 'error',
|
|
71
|
+
'jsx-a11y/iframe-has-title': 'error',
|
|
72
|
+
'jsx-a11y/img-redundant-alt': 'error',
|
|
73
|
+
'jsx-a11y/interactive-supports-focus': 'error',
|
|
74
|
+
'jsx-a11y/label-has-associated-control': 'error',
|
|
75
|
+
'jsx-a11y/media-has-caption': 'error',
|
|
76
|
+
'jsx-a11y/mouse-events-have-key-events': 'error',
|
|
77
|
+
'jsx-a11y/no-access-key': 'error',
|
|
78
|
+
'jsx-a11y/no-autofocus': 'error',
|
|
79
|
+
'jsx-a11y/no-distracting-elements': 'error',
|
|
80
|
+
'jsx-a11y/no-interactive-element-to-noninteractive-role': 'error',
|
|
81
|
+
'jsx-a11y/no-noninteractive-element-interactions': 'error',
|
|
82
|
+
'jsx-a11y/no-noninteractive-element-to-interactive-role': 'error',
|
|
83
|
+
'jsx-a11y/no-noninteractive-tabindex': 'error',
|
|
84
|
+
'jsx-a11y/no-redundant-roles': 'error',
|
|
85
|
+
'jsx-a11y/no-static-element-interactions': 'error',
|
|
86
|
+
'jsx-a11y/role-has-required-aria-props': 'error',
|
|
87
|
+
'jsx-a11y/role-supports-aria-props': 'error',
|
|
88
|
+
'jsx-a11y/scope': 'error',
|
|
89
|
+
'jsx-a11y/tabindex-no-positive': 'error',
|
|
90
|
+
...rules,
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
}
|
package/src/tailwind.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsPlugin } from './js-plugins.js'
|
|
2
|
+
|
|
3
|
+
/** @type {import('oxlint').OxlintConfig} */
|
|
4
|
+
export default {
|
|
5
|
+
jsPlugins: [jsPlugin('shadcn', '@shadcn/lint')],
|
|
6
|
+
rules: {
|
|
7
|
+
'shadcn/no-arbitrary-values': 'error',
|
|
8
|
+
'shadcn/no-inline-styles': 'error',
|
|
9
|
+
'shadcn/no-raw-colors': 'error',
|
|
10
|
+
'shadcn/no-restyle': 'error',
|
|
11
|
+
'shadcn/no-unknown-classes': 'error',
|
|
12
|
+
},
|
|
13
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { reactConfig } from './react.js'
|
|
2
|
+
|
|
3
|
+
const config = reactConfig({ env: { browser: true, node: true } })
|
|
4
|
+
|
|
5
|
+
/** @type {import('oxlint').OxlintConfig} */
|
|
6
|
+
export default {
|
|
7
|
+
...config,
|
|
8
|
+
overrides: [
|
|
9
|
+
{
|
|
10
|
+
files: ['**/routes/**'],
|
|
11
|
+
rules: { 'check-file/folder-naming-convention': 'off', 'unicorn/filename-case': 'off' },
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
}
|