@trigen/oxlint-config 9.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 - present, TrigenSoftware
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # @trigen/oxlint-config
2
+
3
+ [![NPM version][npm]][npm-url]
4
+ [![Node version][node]][node-url]
5
+ [![Dependencies status][deps]][deps-url]
6
+ [![Install size][size]][size-url]
7
+ [![Build status][build]][build-url]
8
+
9
+ [npm]: https://img.shields.io/npm/v/%40trigen/oxlint-config.svg
10
+ [npm-url]: https://npmjs.com/package/@trigen/oxlint-config
11
+
12
+ [node]: https://img.shields.io/node/v/%40trigen/oxlint-config.svg
13
+ [node-url]: https://nodejs.org
14
+
15
+ [deps]: https://img.shields.io/librariesio/release/npm/@trigen/oxlint-config
16
+ [deps-url]: https://libraries.io/npm/@trigen%2Foxlint-config
17
+
18
+ [size]: https://packagephobia.com/badge?p=@trigen/oxlint-config
19
+ [size-url]: https://packagephobia.com/result?p=@trigen/oxlint-config
20
+
21
+ [build]: https://img.shields.io/github/actions/workflow/status/TrigenSoftware/scripts/tests.yml?branch=main
22
+ [build-url]: https://github.com/TrigenSoftware/scripts/actions
23
+
24
+ Trigen's Oxlint config.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pnpm add -D oxlint @trigen/oxlint-config
30
+ # or
31
+ yarn add -D oxlint @trigen/oxlint-config
32
+ # or
33
+ npm i -D oxlint @trigen/oxlint-config
34
+ ```
35
+
36
+ ## Configure
37
+
38
+ Create `oxlint.config.ts` with next content:
39
+
40
+ ```ts
41
+ import baseConfig from '@trigen/oxlint-config'
42
+
43
+ export default baseConfig
44
+ ```
45
+
46
+ ### Additional configs
47
+
48
+ There are additional configs for specific language features:
49
+
50
+ | Config | Description |
51
+ |--------|-------------|
52
+ | @trigen/oxlint-config/commonjs | Rules for CommonJS modules. |
53
+ | @trigen/oxlint-config/module | Rules for ES modules. |
54
+ | @trigen/oxlint-config/bundler | Rules for ES modules with bundler's module resolution. |
55
+ | @trigen/oxlint-config/test | Rules for test files. |
56
+ | @trigen/oxlint-config/react | Rules for React code. |
57
+ | @trigen/oxlint-config/storybook | Rules for Storybook stories. |
58
+ | @trigen/oxlint-config/typescript | Rules for TypeScript code. |
59
+ | @trigen/oxlint-config/typescript-type-checked | Rules for TypeScript code with type checking. |
60
+
61
+ Example:
62
+
63
+ ```ts
64
+ import baseConfig from '@trigen/oxlint-config'
65
+ import bundlerConfig from '@trigen/oxlint-config/bundler'
66
+ import reactConfig from '@trigen/oxlint-config/react'
67
+ import typescriptConfig from '@trigen/oxlint-config/typescript-type-checked'
68
+ import testConfig from '@trigen/oxlint-config/test'
69
+ import storybookConfig from '@trigen/oxlint-config/storybook'
70
+
71
+ export default {
72
+ extends: [
73
+ baseConfig,
74
+ bundlerConfig,
75
+ reactConfig,
76
+ typescriptConfig,
77
+ testConfig,
78
+ storybookConfig
79
+ ]
80
+ }
81
+ ```
@@ -0,0 +1,15 @@
1
+ import moduleConfig from '@trigen/oxlint-config/module'
2
+ import rootConfig from '../../oxlint.config.ts'
3
+
4
+ export default {
5
+ ...rootConfig,
6
+ extends: [
7
+ rootConfig,
8
+ moduleConfig
9
+ ],
10
+ rules: {
11
+ 'eslint/no-magic-numbers': 'off',
12
+ 'import/no-default-export': 'off',
13
+ 'import/no-anonymous-default-export': 'off'
14
+ }
15
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@trigen/oxlint-config",
3
+ "type": "module",
4
+ "version": "9.0.0",
5
+ "description": "Trigen's Oxlint config.",
6
+ "author": "dangreen",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/TrigenSoftware/scripts.git",
11
+ "directory": "packages/oxlint-config"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/TrigenSoftware/scripts/issues"
15
+ },
16
+ "keywords": [
17
+ "oxlint",
18
+ "oxlint-config"
19
+ ],
20
+ "engines": {
21
+ "node": ">=22"
22
+ },
23
+ "exports": {
24
+ ".": "./src/index.js",
25
+ "./plugin": "./src/plugin/index.js",
26
+ "./*": "./src/*.js"
27
+ },
28
+ "peerDependencies": {
29
+ "oxlint": ">=1.0.0",
30
+ "oxlint-tsgolint": ">=0.23.0"
31
+ },
32
+ "peerDependenciesMeta": {
33
+ "oxlint-tsgolint": {
34
+ "optional": true
35
+ }
36
+ },
37
+ "dependencies": {
38
+ "@stylistic/eslint-plugin": "^5.5.0"
39
+ }
40
+ }
package/src/bundler.js ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Bundler override
3
+ */
4
+
5
+ import {
6
+ commonjsFiles,
7
+ not
8
+ } from './subconfigs/files.js'
9
+
10
+ export default {
11
+ overrides: [
12
+ {
13
+ files: not(commonjsFiles),
14
+ plugins: ['import'],
15
+ rules: {
16
+ 'import/unambiguous': 'error',
17
+ 'import/no-commonjs': [
18
+ 'error',
19
+ {
20
+ allowRequire: false,
21
+ allowPrimitiveModules: false
22
+ }
23
+ ]
24
+ }
25
+ }
26
+ ]
27
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * CommonJS override
3
+ */
4
+
5
+ import moduleConfig from './module.js'
6
+ import { moduleFiles } from './subconfigs/files.js'
7
+
8
+ export default {
9
+ overrides: moduleConfig.overrides.map(override => ({
10
+ ...override,
11
+ files: moduleFiles
12
+ }))
13
+ }
package/src/index.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Combine all basic configs
3
+ */
4
+
5
+ import basicConfig from './subconfigs/basic.js'
6
+ import importConfig from './subconfigs/import.js'
7
+ import baseStylisticConfig from './subconfigs/base.stylistic.js'
8
+ import jsdocConfig from './subconfigs/jsdoc.js'
9
+ import configsConfig from './subconfigs/configs.js'
10
+
11
+ export default {
12
+ extends: [
13
+ basicConfig,
14
+ importConfig,
15
+ baseStylisticConfig,
16
+ jsdocConfig,
17
+ configsConfig
18
+ ]
19
+ }
package/src/module.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * ESM override
3
+ */
4
+
5
+ import bundlerConfig from './bundler.js'
6
+ import {
7
+ commonjsFiles,
8
+ not
9
+ } from './subconfigs/files.js'
10
+
11
+ export default {
12
+ overrides: [
13
+ ...bundlerConfig.overrides,
14
+ {
15
+ files: not(commonjsFiles),
16
+ plugins: ['import'],
17
+ rules: {
18
+ 'import/extensions': ['error', 'ignorePackages']
19
+ }
20
+ }
21
+ ]
22
+ }
@@ -0,0 +1,357 @@
1
+ import { builtinModules } from 'node:module'
2
+
3
+ const builtinModuleNames = new Set(
4
+ builtinModules.flatMap(moduleName => [
5
+ moduleName,
6
+ moduleName.replace(/^node:/, '')
7
+ ])
8
+ )
9
+ const defaultGroups = [
10
+ 'builtin',
11
+ 'external',
12
+ 'internal',
13
+ 'parent',
14
+ 'sibling',
15
+ 'index'
16
+ ]
17
+ const defaultOptions = {
18
+ 'groups': defaultGroups,
19
+ 'pathGroups': [],
20
+ 'newlines-between': 'ignore',
21
+ 'typeImports': 'none'
22
+ }
23
+
24
+ function isBuiltin(source) {
25
+ return source.startsWith('node:')
26
+ || builtinModuleNames.has(source.split('/')[0])
27
+ }
28
+
29
+ function isExternal(source) {
30
+ return !source.startsWith('.')
31
+ && !source.startsWith('/')
32
+ && !source.startsWith('~')
33
+ && !source.startsWith('#')
34
+ && !source.startsWith('@/')
35
+ }
36
+
37
+ function isIndex(source) {
38
+ return source === '.'
39
+ || source === './'
40
+ || source === './index'
41
+ || source.startsWith('./index.')
42
+ }
43
+
44
+ function getGroup(source) {
45
+ if (isBuiltin(source)) {
46
+ return 'builtin'
47
+ }
48
+
49
+ if (isExternal(source)) {
50
+ return 'external'
51
+ }
52
+
53
+ if (source.startsWith('../')) {
54
+ return 'parent'
55
+ }
56
+
57
+ if (isIndex(source)) {
58
+ return 'index'
59
+ }
60
+
61
+ if (source.startsWith('./')) {
62
+ return 'sibling'
63
+ }
64
+
65
+ return 'internal'
66
+ }
67
+
68
+ function matchesPathGroup(source, pattern) {
69
+ if (pattern === '~/**') {
70
+ return source.startsWith('~/')
71
+ }
72
+
73
+ if (pattern === '#*/**') {
74
+ return /^#[^/]+\/.+/.test(source)
75
+ }
76
+
77
+ if (pattern === '@/**') {
78
+ return source.startsWith('@/') || source === '@'
79
+ }
80
+
81
+ return false
82
+ }
83
+
84
+ function getPathGroup(source, pathGroups) {
85
+ return pathGroups.find(({ pattern }) => matchesPathGroup(source, pattern))
86
+ }
87
+
88
+ function isTypeImport(node) {
89
+ return node.importKind === 'type'
90
+ || (
91
+ node.specifiers.length > 0
92
+ && node.specifiers.every(specifier => specifier.importKind === 'type')
93
+ )
94
+ }
95
+
96
+ function getTypeRank(node, options) {
97
+ if (options.typeImports === 'first') {
98
+ return isTypeImport(node) ? 0 : 1
99
+ }
100
+
101
+ if (options.typeImports === 'last') {
102
+ return isTypeImport(node) ? 1 : 0
103
+ }
104
+
105
+ return 0
106
+ }
107
+
108
+ function getImportKind(node) {
109
+ return isTypeImport(node) ? 'type' : 'value'
110
+ }
111
+
112
+ function getRank(node, options) {
113
+ const source = node.source.value
114
+ const pathGroup = getPathGroup(source, options.pathGroups)
115
+ const group = pathGroup?.group ?? getGroup(source)
116
+ const groupIndex = options.groups.indexOf(group)
117
+ const positionRank = pathGroup?.position === 'before'
118
+ ? 0
119
+ : pathGroup?.position === 'after'
120
+ ? 2
121
+ : 1
122
+
123
+ return {
124
+ group,
125
+ groupIndex: groupIndex === -1 ? Number.POSITIVE_INFINITY : groupIndex,
126
+ positionRank,
127
+ importKind: getImportKind(node),
128
+ typeRank: getTypeRank(node, options)
129
+ }
130
+ }
131
+
132
+ function compareRanks(left, right) {
133
+ return left.groupIndex - right.groupIndex
134
+ || left.positionRank - right.positionRank
135
+ || left.typeRank - right.typeRank
136
+ }
137
+
138
+ function getLinebreak(text) {
139
+ return text.includes('\r\n') ? '\r\n' : '\n'
140
+ }
141
+
142
+ function getMessage(left, right) {
143
+ if (
144
+ left.groupIndex === right.groupIndex
145
+ && left.positionRank === right.positionRank
146
+ && left.typeRank !== right.typeRank
147
+ ) {
148
+ return `Expected ${right.importKind} import to come before ${left.importKind} import.`
149
+ }
150
+
151
+ return `Expected ${right.group} import to come before ${left.group} import.`
152
+ }
153
+
154
+ function getOptions(context) {
155
+ return {
156
+ ...defaultOptions,
157
+ ...context.options[0],
158
+ groups: context.options[0]?.groups ?? defaultGroups,
159
+ pathGroups: context.options[0]?.pathGroups ?? []
160
+ }
161
+ }
162
+
163
+ function getImportItems(imports, options) {
164
+ return imports.map((node, index) => ({
165
+ index,
166
+ node,
167
+ rank: getRank(node, options)
168
+ }))
169
+ }
170
+
171
+ function compareImportItems(left, right) {
172
+ return compareRanks(left.rank, right.rank)
173
+ || left.index - right.index
174
+ }
175
+
176
+ function getFirstUnorderedPair(items) {
177
+ for (let index = 1; index < items.length; index++) {
178
+ const previousItem = items[index - 1]
179
+ const item = items[index]
180
+
181
+ if (compareRanks(previousItem.rank, item.rank) > 0) {
182
+ return [
183
+ previousItem,
184
+ item
185
+ ]
186
+ }
187
+ }
188
+
189
+ return null
190
+ }
191
+
192
+ function hasInvalidNewlines(items, options) {
193
+ if (options['newlines-between'] === 'ignore') {
194
+ return false
195
+ }
196
+
197
+ return items.some((item, index) => {
198
+ if (index === 0) {
199
+ return false
200
+ }
201
+
202
+ const previousItem = items[index - 1]
203
+ const hasEmptyLine = item.node.loc.start.line - previousItem.node.loc.end.line > 1
204
+
205
+ return options['newlines-between'] === 'never'
206
+ ? hasEmptyLine
207
+ : !hasEmptyLine
208
+ })
209
+ }
210
+
211
+ function hasInnerComments(sourceCode, items) {
212
+ const firstNode = items[0].node
213
+ const lastNode = items.at(-1).node
214
+
215
+ return sourceCode.getAllComments().some(comment => comment.range[0] > firstNode.range[0]
216
+ && comment.range[1] < lastNode.range[1])
217
+ }
218
+
219
+ function hasSideEffectImports(items) {
220
+ return items.some(({ node }) => node.specifiers.length === 0)
221
+ }
222
+
223
+ function canFix(sourceCode, items) {
224
+ return !hasSideEffectImports(items)
225
+ && !hasInnerComments(sourceCode, items)
226
+ }
227
+
228
+ function getFixedImportText(sourceCode, items, options) {
229
+ const linebreak = getLinebreak(sourceCode.text)
230
+ const separator = options['newlines-between'] === 'always'
231
+ ? `${linebreak}${linebreak}`
232
+ : linebreak
233
+
234
+ return [...items]
235
+ .sort(compareImportItems)
236
+ .map(({ node }) => sourceCode.getText(node))
237
+ .join(separator)
238
+ }
239
+
240
+ function getImportBlockRange(items) {
241
+ return [
242
+ items[0].node.range[0],
243
+ items.at(-1).node.range[1]
244
+ ]
245
+ }
246
+
247
+ export default {
248
+ meta: {
249
+ type: 'layout',
250
+ fixable: 'code',
251
+ docs: {
252
+ description: 'Enforce import order by configured groups.'
253
+ },
254
+ schema: [
255
+ {
256
+ type: 'object',
257
+ properties: {
258
+ 'groups': {
259
+ type: 'array',
260
+ items: {
261
+ type: 'string'
262
+ }
263
+ },
264
+ 'pathGroups': {
265
+ type: 'array',
266
+ items: {
267
+ type: 'object',
268
+ properties: {
269
+ pattern: {
270
+ type: 'string'
271
+ },
272
+ group: {
273
+ type: 'string'
274
+ },
275
+ position: {
276
+ enum: [
277
+ 'before',
278
+ 'after'
279
+ ]
280
+ }
281
+ },
282
+ required: ['pattern', 'group'],
283
+ additionalProperties: true
284
+ }
285
+ },
286
+ 'newlines-between': {
287
+ enum: [
288
+ 'ignore',
289
+ 'always',
290
+ 'never'
291
+ ]
292
+ },
293
+ 'typeImports': {
294
+ enum: [
295
+ 'first',
296
+ 'last',
297
+ 'none'
298
+ ]
299
+ }
300
+ },
301
+ additionalProperties: true
302
+ }
303
+ ]
304
+ },
305
+ create(context) {
306
+ const options = getOptions(context)
307
+ const imports = []
308
+
309
+ return {
310
+ ImportDeclaration(node) {
311
+ const source = node.source.value
312
+
313
+ if (typeof source !== 'string') {
314
+ return
315
+ }
316
+
317
+ imports.push(node)
318
+ },
319
+ 'Program:exit'(node) {
320
+ if (imports.length < 2) {
321
+ return
322
+ }
323
+
324
+ const sourceCode = context.sourceCode
325
+ const items = getImportItems(imports, options)
326
+ const unorderedPair = getFirstUnorderedPair(items)
327
+ const invalidNewlines = hasInvalidNewlines(items, options)
328
+
329
+ if (!unorderedPair && !invalidNewlines) {
330
+ return
331
+ }
332
+
333
+ const fix = canFix(sourceCode, items)
334
+ ? fixer => fixer.replaceTextRange(
335
+ getImportBlockRange(items),
336
+ getFixedImportText(sourceCode, items, options)
337
+ )
338
+ : null
339
+ const [
340
+ previousItem,
341
+ item
342
+ ] = unorderedPair ?? [
343
+ items[0],
344
+ items[1]
345
+ ]
346
+
347
+ context.report({
348
+ node,
349
+ message: unorderedPair
350
+ ? getMessage(previousItem.rank, item.rank)
351
+ : 'Import declarations have invalid empty lines.',
352
+ fix
353
+ })
354
+ }
355
+ }
356
+ }
357
+ }
@@ -0,0 +1,16 @@
1
+ import importOrderRule from './import-order.js'
2
+ import memberOrderingRule from './member-ordering.js'
3
+ import namedImportOrderRule from './named-import-order.js'
4
+ import namingConventionRule from './naming-convention.js'
5
+
6
+ export default {
7
+ meta: {
8
+ name: '@trigen/oxlint-config/plugin'
9
+ },
10
+ rules: {
11
+ 'import-order': importOrderRule,
12
+ 'member-ordering': memberOrderingRule,
13
+ 'named-import-order': namedImportOrderRule,
14
+ 'naming-convention': namingConventionRule
15
+ }
16
+ }