oxlint-plugin-arch 0.2.1
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/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +101 -0
- package/package.json +64 -0
- package/src/index.ts +57 -0
- package/src/rules/export-file-prefix.ts +70 -0
- package/src/rules/export-name-pattern.ts +71 -0
- package/src/rules/filename-export-name.ts +86 -0
- package/src/rules/filename-match.ts +47 -0
- package/src/rules/folder-prefix.ts +73 -0
- package/src/rules/no-extra-exports.ts +60 -0
- package/src/rules/no-extra-factory-keys.ts +58 -0
- package/src/rules/no-file-level-helpers.ts +58 -0
- package/src/rules/no-imported-type-alias.ts +83 -0
- package/src/rules/no-inline-schema-elements.ts +70 -0
- package/src/rules/no-local-schema-construction.ts +162 -0
- package/src/rules/no-rederive-schema.ts +66 -0
- package/src/rules/no-restricted-files.ts +40 -0
- package/src/rules/no-restricted-token.ts +71 -0
- package/src/rules/no-runtime-in-types.ts +72 -0
- package/src/rules/no-single-use-scalar-schema.ts +164 -0
- package/src/rules/no-top-level-functions.ts +77 -0
- package/src/rules/no-trivial-functions.ts +56 -0
- package/src/rules/no-unescaped-like.ts +67 -0
- package/src/rules/only-export-components.ts +78 -0
- package/src/rules/require-file-factory.ts +67 -0
- package/src/rules/require-object-params.ts +32 -0
- package/src/rules/require-orpc-output.ts +64 -0
- package/src/rules/require-paired-call.ts +71 -0
- package/src/rules/route-surface.ts +62 -0
- package/src/utils/helpers/ast.ts +101 -0
- package/src/utils/helpers/declarations.ts +203 -0
- package/src/utils/helpers/exports.ts +158 -0
- package/src/utils/helpers/factories.ts +67 -0
- package/src/utils/helpers/index.ts +11 -0
- package/src/utils/helpers/naming.ts +89 -0
- package/src/utils/helpers/options.ts +5 -0
- package/src/utils/helpers/orpc.ts +31 -0
- package/src/utils/helpers/react-components.ts +146 -0
- package/src/utils/helpers/routes.ts +31 -0
- package/src/utils/helpers/sanitizers.ts +33 -0
- package/src/utils/helpers/schemas.ts +82 -0
- package/src/utils/index.ts +1 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
First npm release.
|
|
6
|
+
|
|
7
|
+
- Add `no-trivial-functions`, `no-local-schema-construction`, and `no-single-use-scalar-schema`.
|
|
8
|
+
- Add `folder-prefix` `after` so nested folders stay in the filename.
|
|
9
|
+
- Add `allDeclarations` on filename/export naming rules.
|
|
10
|
+
- Close alias, type-only import, re-export, and `zod/*` edge cases.
|
|
11
|
+
- Install from npm with specifier `oxlint-plugin-arch`. Vendoring stays optional.
|
|
12
|
+
|
|
13
|
+
## 0.1.0
|
|
14
|
+
|
|
15
|
+
Initial plugin with configurable architecture rules.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 jon
|
|
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,101 @@
|
|
|
1
|
+
# oxlint-plugin-arch
|
|
2
|
+
|
|
3
|
+
Configurable, filename-aware architecture rules for Oxlint.
|
|
4
|
+
|
|
5
|
+
Install the package, then put repository paths and policy in your Oxlint config. Rules contain no application-specific names or folders.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun add -d oxlint-plugin-arch oxlint @oxlint/plugins
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Use matching `oxlint` and `@oxlint/plugins` versions. Then register the package:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { defineConfig } from 'oxlint'
|
|
17
|
+
|
|
18
|
+
export default defineConfig({
|
|
19
|
+
jsPlugins: [
|
|
20
|
+
{ name: 'arch', specifier: 'oxlint-plugin-arch' },
|
|
21
|
+
],
|
|
22
|
+
})
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Recommended companion: anti-slop
|
|
26
|
+
|
|
27
|
+
We recommend [Dillon Mulroy's anti-slop](https://github.com/dmmulroy/anti-slop) alongside this plugin. Anti-slop catches low-evidence TypeScript and JavaScript implementation patterns; oxlint-plugin-arch enforces configurable file, export, boundary, and API structure.
|
|
28
|
+
|
|
29
|
+
Register both in the same Oxlint config under their separate `anti-slop/*` and `arch/*` rule namespaces.
|
|
30
|
+
|
|
31
|
+
## Optional: vendor a local copy
|
|
32
|
+
|
|
33
|
+
If you want to inspect or fork the rules in-tree, copy `src/index.ts`, `src/rules/`, and `src/utils/`, or use the agent skill:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx skills add nikuscs/oxlint-plugin-arch --skill install-oxlint-arch
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then point `jsPlugins[].specifier` at the copied entry file instead of the package name.
|
|
40
|
+
|
|
41
|
+
## Configuration examples
|
|
42
|
+
|
|
43
|
+
- [`examples/minimal.oxlint.config.ts`](examples/minimal.oxlint.config.ts) — three small rules showing plugin registration, glob ownership, and options.
|
|
44
|
+
- [`examples/full.oxlint.config.ts`](examples/full.oxlint.config.ts) — all 25 rules across representative component, action, service, route, API, schema, and database scopes.
|
|
45
|
+
|
|
46
|
+
Copy the shapes that match your repository; do not copy globs or naming policy blindly.
|
|
47
|
+
|
|
48
|
+
## Rules
|
|
49
|
+
|
|
50
|
+
- `export-file-prefix` — require export names, or every function and type, to start with a filename-derived prefix.
|
|
51
|
+
- `export-name-pattern` — require export names, or every function and type, to match a configured regular expression.
|
|
52
|
+
- `filename-export-name` — derive expected function names from filename templates, optionally including locals.
|
|
53
|
+
- `filename-match` — require filenames to match a configured pattern.
|
|
54
|
+
- `folder-prefix` — require filenames to start with their parent folder name, or the folders after a configured root.
|
|
55
|
+
- `no-extra-exports` — restrict files to configured export templates.
|
|
56
|
+
- `no-extra-factory-keys` — restrict direct factory return keys.
|
|
57
|
+
- `no-file-level-helpers` — keep unapproved helpers out of module scope.
|
|
58
|
+
- `no-imported-type-alias` — reject exported aliases that only rename imported types.
|
|
59
|
+
- `no-inline-schema-elements` — require named schemas inside configured combinators.
|
|
60
|
+
- `no-local-schema-construction` — reject runtime schema-library imports and local schema construction, with consumer-chosen severity, messages, and path exceptions.
|
|
61
|
+
- `no-rederive-schema` — reject type derivation from configured imported schemas.
|
|
62
|
+
- `no-restricted-files` — reject files selected by a consumer-owned forbidden glob.
|
|
63
|
+
- `no-restricted-token` — restrict an identifier to configured owner paths.
|
|
64
|
+
- `no-runtime-in-types` — keep selected type modules runtime-free.
|
|
65
|
+
- `no-single-use-scalar-schema` — reject local scalar Zod aliases used once where they can be safely inlined.
|
|
66
|
+
- `no-top-level-functions` — reject top-level functions and optional re-exports.
|
|
67
|
+
- `no-trivial-functions` — reject empty or passthrough top-level functions.
|
|
68
|
+
- `no-unescaped-like` — require configured sanitizers for configured query methods.
|
|
69
|
+
- `only-export-components` — allow only React component and type exports.
|
|
70
|
+
- `require-file-factory` — derive and require a filename-based factory function.
|
|
71
|
+
- `require-object-params` — require exported functions to use object-shaped parameters.
|
|
72
|
+
- `require-orpc-output` — require named oRPC output schemas.
|
|
73
|
+
- `require-paired-call` — require one configured call when another appears.
|
|
74
|
+
- `route-surface` — constrain route exports, hooks, and intrinsic JSX.
|
|
75
|
+
|
|
76
|
+
Every rule file includes plain-English behavior and examples. Rule tests under `src/tests/` show complete option shapes.
|
|
77
|
+
|
|
78
|
+
## Development
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
bun install
|
|
82
|
+
bun run check
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
After production changes, run `bun run sync:skill-assets`; CI verifies the bundled installer copy matches `src/`.
|
|
86
|
+
|
|
87
|
+
## Release
|
|
88
|
+
|
|
89
|
+
Update `CHANGELOG.md`, then run the full lifecycle from a clean `main`:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
bun run release # prompt for patch/minor/major
|
|
93
|
+
bun run release:patch # check, bump, tag, push, npm publish
|
|
94
|
+
bun run release:dry-run # check + npm pack dry-run only
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`scripts/release.sh` refuses dirty trees and requires `npm login`. `prepublishOnly` runs `bun run check` again before the tarball goes out.
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "oxlint-plugin-arch",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Configurable architecture rules for Oxlint JavaScript plugins.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"CHANGELOG.md",
|
|
12
|
+
"src/index.ts",
|
|
13
|
+
"src/rules",
|
|
14
|
+
"src/utils"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"check": "bun run lint && bun run test && bun run typecheck && bun run check:examples && bun run check:skill-assets",
|
|
18
|
+
"lint": "oxlint src examples scripts skills/install-oxlint-arch/scripts",
|
|
19
|
+
"test": "vitest run src/tests",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"sync:skill-assets": "bun scripts/sync-skill-assets.ts",
|
|
22
|
+
"check:examples": "oxlint examples/minimal.oxlint.config.ts -c examples/minimal.oxlint.config.ts --disable-nested-config && oxlint examples/full.oxlint.config.ts -c examples/full.oxlint.config.ts --disable-nested-config",
|
|
23
|
+
"check:skill-assets": "bun scripts/sync-skill-assets.ts --check",
|
|
24
|
+
"prepublishOnly": "bun run check",
|
|
25
|
+
"release": "bash scripts/release.sh",
|
|
26
|
+
"release:dry-run": "bash scripts/release.sh --dry-run",
|
|
27
|
+
"release:patch": "bash scripts/release.sh patch --yes",
|
|
28
|
+
"release:minor": "bash scripts/release.sh minor --yes",
|
|
29
|
+
"release:major": "bash scripts/release.sh major --yes"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@oxlint/plugins": "1.78.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"oxlint": ">=1.78.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "26.0.1",
|
|
39
|
+
"bumpp": "10.4.1",
|
|
40
|
+
"oxlint": "1.78.0",
|
|
41
|
+
"typescript": "6.0.3",
|
|
42
|
+
"vitest": "4.1.9"
|
|
43
|
+
},
|
|
44
|
+
"packageManager": "bun@1.3.14",
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/nikuscs/oxlint-plugin-arch.git"
|
|
52
|
+
},
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/nikuscs/oxlint-plugin-arch/issues"
|
|
55
|
+
},
|
|
56
|
+
"homepage": "https://github.com/nikuscs/oxlint-plugin-arch#readme",
|
|
57
|
+
"keywords": [
|
|
58
|
+
"oxlint",
|
|
59
|
+
"eslint",
|
|
60
|
+
"architecture",
|
|
61
|
+
"lint",
|
|
62
|
+
"plugin"
|
|
63
|
+
]
|
|
64
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { eslintCompatPlugin } from '@oxlint/plugins'
|
|
2
|
+
import { exportFilePrefix } from './rules/export-file-prefix.ts'
|
|
3
|
+
import { exportNamePattern } from './rules/export-name-pattern.ts'
|
|
4
|
+
import { filenameExportName } from './rules/filename-export-name.ts'
|
|
5
|
+
import { filenameMatch } from './rules/filename-match.ts'
|
|
6
|
+
import { folderPrefix } from './rules/folder-prefix.ts'
|
|
7
|
+
import { noExtraExports } from './rules/no-extra-exports.ts'
|
|
8
|
+
import { noExtraFactoryKeys } from './rules/no-extra-factory-keys.ts'
|
|
9
|
+
import { noFileLevelHelpers } from './rules/no-file-level-helpers.ts'
|
|
10
|
+
import { noImportedTypeAlias } from './rules/no-imported-type-alias.ts'
|
|
11
|
+
import { noInlineSchemaElements } from './rules/no-inline-schema-elements.ts'
|
|
12
|
+
import { noLocalSchemaConstruction } from './rules/no-local-schema-construction.ts'
|
|
13
|
+
import { noRederiveSchema } from './rules/no-rederive-schema.ts'
|
|
14
|
+
import { noRestrictedFiles } from './rules/no-restricted-files.ts'
|
|
15
|
+
import { noRestrictedToken } from './rules/no-restricted-token.ts'
|
|
16
|
+
import { noRuntimeInTypes } from './rules/no-runtime-in-types.ts'
|
|
17
|
+
import { noSingleUseScalarSchema } from './rules/no-single-use-scalar-schema.ts'
|
|
18
|
+
import { noTopLevelFunctions } from './rules/no-top-level-functions.ts'
|
|
19
|
+
import { noTrivialFunctions } from './rules/no-trivial-functions.ts'
|
|
20
|
+
import { noUnescapedLike } from './rules/no-unescaped-like.ts'
|
|
21
|
+
import { onlyExportComponents } from './rules/only-export-components.ts'
|
|
22
|
+
import { requireFileFactory } from './rules/require-file-factory.ts'
|
|
23
|
+
import { requireObjectParams } from './rules/require-object-params.ts'
|
|
24
|
+
import { requireOrpcOutput } from './rules/require-orpc-output.ts'
|
|
25
|
+
import { requirePairedCall } from './rules/require-paired-call.ts'
|
|
26
|
+
import { routeSurface } from './rules/route-surface.ts'
|
|
27
|
+
|
|
28
|
+
export default eslintCompatPlugin({
|
|
29
|
+
meta: { name: 'arch' },
|
|
30
|
+
rules: {
|
|
31
|
+
'export-file-prefix': exportFilePrefix,
|
|
32
|
+
'export-name-pattern': exportNamePattern,
|
|
33
|
+
'filename-export-name': filenameExportName,
|
|
34
|
+
'filename-match': filenameMatch,
|
|
35
|
+
'folder-prefix': folderPrefix,
|
|
36
|
+
'no-extra-exports': noExtraExports,
|
|
37
|
+
'no-extra-factory-keys': noExtraFactoryKeys,
|
|
38
|
+
'no-file-level-helpers': noFileLevelHelpers,
|
|
39
|
+
'no-imported-type-alias': noImportedTypeAlias,
|
|
40
|
+
'no-inline-schema-elements': noInlineSchemaElements,
|
|
41
|
+
'no-local-schema-construction': noLocalSchemaConstruction,
|
|
42
|
+
'no-rederive-schema': noRederiveSchema,
|
|
43
|
+
'no-restricted-files': noRestrictedFiles,
|
|
44
|
+
'no-restricted-token': noRestrictedToken,
|
|
45
|
+
'no-runtime-in-types': noRuntimeInTypes,
|
|
46
|
+
'no-single-use-scalar-schema': noSingleUseScalarSchema,
|
|
47
|
+
'no-top-level-functions': noTopLevelFunctions,
|
|
48
|
+
'no-trivial-functions': noTrivialFunctions,
|
|
49
|
+
'no-unescaped-like': noUnescapedLike,
|
|
50
|
+
'only-export-components': onlyExportComponents,
|
|
51
|
+
'require-file-factory': requireFileFactory,
|
|
52
|
+
'require-object-params': requireObjectParams,
|
|
53
|
+
'require-orpc-output': requireOrpcOutput,
|
|
54
|
+
'require-paired-call': requirePairedCall,
|
|
55
|
+
'route-surface': routeSurface,
|
|
56
|
+
},
|
|
57
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { defineRule } from '@oxlint/plugins'
|
|
2
|
+
import { declarationsCollectNamed, exportsCollect, namingFileBasename, optionsFirst } from '../utils/index.ts'
|
|
3
|
+
|
|
4
|
+
interface ExportFilePrefixOptions {
|
|
5
|
+
stem?: 'before-first-dot' | 'full-basename'
|
|
6
|
+
normalize?: 'remove-separators' | 'none'
|
|
7
|
+
allDeclarations?: boolean
|
|
8
|
+
allowPattern?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Checks that exported names start with a prefix taken from the filename, and can also check every function and type in the file.
|
|
13
|
+
*
|
|
14
|
+
* Example: In `foo-chart.tsx`, `fooChartPreview` and `interface FooChartPreviewProps` pass; `preview` fails when `allDeclarations` is on.
|
|
15
|
+
*/
|
|
16
|
+
export const exportFilePrefix = defineRule({
|
|
17
|
+
meta: {
|
|
18
|
+
type: 'problem',
|
|
19
|
+
schema: [{
|
|
20
|
+
type: 'object',
|
|
21
|
+
additionalProperties: false,
|
|
22
|
+
properties: {
|
|
23
|
+
stem: { type: 'string', enum: ['before-first-dot', 'full-basename'] },
|
|
24
|
+
normalize: { type: 'string', enum: ['remove-separators', 'none'] },
|
|
25
|
+
allDeclarations: { type: 'boolean' },
|
|
26
|
+
allowPattern: { type: 'string' },
|
|
27
|
+
},
|
|
28
|
+
}],
|
|
29
|
+
messages: {
|
|
30
|
+
prefix: "'{{name}}' must start with file prefix '{{prefix}}'.",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
createOnce(context) {
|
|
34
|
+
return {
|
|
35
|
+
Program(program) {
|
|
36
|
+
const options = optionsFirst<ExportFilePrefixOptions>(context, {
|
|
37
|
+
stem: 'before-first-dot',
|
|
38
|
+
normalize: 'remove-separators',
|
|
39
|
+
})
|
|
40
|
+
const basename = namingFileBasename(context.filename).replace(/\.(tsx?|jsx?)$/, '')
|
|
41
|
+
const stem = options.stem === 'full-basename' ? basename : (basename.split('.')[0] ?? '')
|
|
42
|
+
const prefix = options.normalize === 'none' ? stem : stem.replaceAll(/[-_]/g, '').toLowerCase()
|
|
43
|
+
const allowed = options.allowPattern ? new RegExp(options.allowPattern) : null
|
|
44
|
+
const names = options.allDeclarations
|
|
45
|
+
? declarationsCollectNamed(program)
|
|
46
|
+
: exportsCollect(program).map((binding) => ({
|
|
47
|
+
name: binding.localName ?? binding.exportedName,
|
|
48
|
+
node: binding.node,
|
|
49
|
+
}))
|
|
50
|
+
const seen = new Set<string>()
|
|
51
|
+
|
|
52
|
+
for (const item of names) {
|
|
53
|
+
const key = `${item.name}:${item.node.start}:${item.node.end}`
|
|
54
|
+
const comparableName = options.normalize === 'none' ? item.name : item.name.replaceAll('_', '').toLowerCase()
|
|
55
|
+
|
|
56
|
+
if (seen.has(key) || allowed?.test(item.name) || comparableName.startsWith(prefix)) {
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
seen.add(key)
|
|
61
|
+
context.report({
|
|
62
|
+
node: item.node,
|
|
63
|
+
messageId: 'prefix',
|
|
64
|
+
data: { name: item.name, prefix },
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { defineRule } from '@oxlint/plugins'
|
|
2
|
+
import { declarationsCollectNamed, exportsCollect, optionsFirst } from '../utils/index.ts'
|
|
3
|
+
|
|
4
|
+
interface ExportNamePatternOptions {
|
|
5
|
+
pattern: string
|
|
6
|
+
flags?: string
|
|
7
|
+
ignoreTypeExports?: boolean
|
|
8
|
+
allDeclarations?: boolean
|
|
9
|
+
allowPattern?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Checks exported names, or every function and type, against a regular expression supplied in the rule options.
|
|
14
|
+
*
|
|
15
|
+
* Example: With `^user[A-Z]`, `userGet` passes while `getUser` fails.
|
|
16
|
+
*/
|
|
17
|
+
export const exportNamePattern = defineRule({
|
|
18
|
+
meta: {
|
|
19
|
+
type: 'problem',
|
|
20
|
+
schema: [{
|
|
21
|
+
type: 'object',
|
|
22
|
+
additionalProperties: false,
|
|
23
|
+
properties: {
|
|
24
|
+
pattern: { type: 'string' },
|
|
25
|
+
flags: { type: 'string' },
|
|
26
|
+
ignoreTypeExports: { type: 'boolean' },
|
|
27
|
+
allDeclarations: { type: 'boolean' },
|
|
28
|
+
allowPattern: { type: 'string' },
|
|
29
|
+
},
|
|
30
|
+
required: ['pattern'],
|
|
31
|
+
}],
|
|
32
|
+
messages: {
|
|
33
|
+
pattern: "'{{name}}' must match {{pattern}}.",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
createOnce(context) {
|
|
37
|
+
return {
|
|
38
|
+
Program(program) {
|
|
39
|
+
const {
|
|
40
|
+
pattern,
|
|
41
|
+
flags,
|
|
42
|
+
ignoreTypeExports = false,
|
|
43
|
+
allDeclarations = false,
|
|
44
|
+
allowPattern,
|
|
45
|
+
} = optionsFirst<ExportNamePatternOptions>(context)
|
|
46
|
+
const allowed = allowPattern ? new RegExp(allowPattern) : null
|
|
47
|
+
const names = allDeclarations
|
|
48
|
+
? declarationsCollectNamed(program)
|
|
49
|
+
: exportsCollect(program).flatMap((binding) => ignoreTypeExports && binding.typeOnly
|
|
50
|
+
? []
|
|
51
|
+
: [{ name: binding.localName ?? binding.exportedName, node: binding.node }])
|
|
52
|
+
const seen = new Set<string>()
|
|
53
|
+
|
|
54
|
+
for (const item of names) {
|
|
55
|
+
const key = `${item.name}:${item.node.start}:${item.node.end}`
|
|
56
|
+
|
|
57
|
+
if (seen.has(key) || allowed?.test(item.name) || new RegExp(pattern, flags).test(item.name)) {
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
seen.add(key)
|
|
62
|
+
context.report({
|
|
63
|
+
node: item.node,
|
|
64
|
+
messageId: 'pattern',
|
|
65
|
+
data: { name: item.name, pattern },
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
})
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { defineRule } from '@oxlint/plugins'
|
|
2
|
+
import {
|
|
3
|
+
declarationsCollectFunctions,
|
|
4
|
+
exportsCollectFunctions,
|
|
5
|
+
factoriesExpandTemplate,
|
|
6
|
+
namingFileBasename,
|
|
7
|
+
namingMatchTemplate,
|
|
8
|
+
optionsFirst,
|
|
9
|
+
} from '../utils/index.ts'
|
|
10
|
+
|
|
11
|
+
interface FilenameExportNameOptions {
|
|
12
|
+
file: string
|
|
13
|
+
export: string
|
|
14
|
+
mode?: 'all' | 'some'
|
|
15
|
+
placeholderPattern?: string
|
|
16
|
+
allDeclarations?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Builds an expected function name from filename placeholders, then checks exported or all functions against it.
|
|
21
|
+
*
|
|
22
|
+
* Example: `user-action.create.ts` can require `makeUserActionCreate`; `mode: some` allows additional exports.
|
|
23
|
+
*/
|
|
24
|
+
export const filenameExportName = defineRule({
|
|
25
|
+
meta: {
|
|
26
|
+
type: 'problem',
|
|
27
|
+
schema: [{
|
|
28
|
+
type: 'object',
|
|
29
|
+
additionalProperties: false,
|
|
30
|
+
properties: {
|
|
31
|
+
file: { type: 'string' },
|
|
32
|
+
export: { type: 'string' },
|
|
33
|
+
mode: { type: 'string', enum: ['all', 'some'] },
|
|
34
|
+
placeholderPattern: { type: 'string' },
|
|
35
|
+
allDeclarations: { type: 'boolean' },
|
|
36
|
+
},
|
|
37
|
+
required: ['file', 'export'],
|
|
38
|
+
}],
|
|
39
|
+
messages: {
|
|
40
|
+
mismatch: "Function '{{actual}}' must match '{{expected}}'.",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
createOnce(context) {
|
|
44
|
+
return {
|
|
45
|
+
Program(program) {
|
|
46
|
+
const {
|
|
47
|
+
file,
|
|
48
|
+
export: exportTemplate,
|
|
49
|
+
mode = 'all',
|
|
50
|
+
placeholderPattern,
|
|
51
|
+
allDeclarations = false,
|
|
52
|
+
} = optionsFirst<FilenameExportNameOptions>(context)
|
|
53
|
+
const groups = namingMatchTemplate(namingFileBasename(context.filename), file, placeholderPattern)
|
|
54
|
+
|
|
55
|
+
if (!groups) {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const expected = factoriesExpandTemplate(exportTemplate, groups)
|
|
60
|
+
const functions = allDeclarations ? declarationsCollectFunctions(program) : exportsCollectFunctions(program)
|
|
61
|
+
|
|
62
|
+
if (mode === 'some') {
|
|
63
|
+
if (!functions.some((item) => item.name === expected)) {
|
|
64
|
+
const first = functions.at(0)
|
|
65
|
+
context.report({
|
|
66
|
+
node: first?.node ?? program,
|
|
67
|
+
messageId: 'mismatch',
|
|
68
|
+
data: { actual: first?.name ?? '<none>', expected },
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const item of functions) {
|
|
75
|
+
if (item.name !== expected) {
|
|
76
|
+
context.report({
|
|
77
|
+
node: item.node,
|
|
78
|
+
messageId: 'mismatch',
|
|
79
|
+
data: { actual: item.name, expected },
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { defineRule } from '@oxlint/plugins'
|
|
2
|
+
import { namingFileBasename, optionsFirst } from '../utils/index.ts'
|
|
3
|
+
|
|
4
|
+
interface FilenameMatchOptions {
|
|
5
|
+
pattern: string
|
|
6
|
+
message: string
|
|
7
|
+
flags?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Checks the current filename against a configurable regular expression and reports the configured message.
|
|
12
|
+
*
|
|
13
|
+
* Example: A pattern ending in `.spec.ts` accepts `login.spec.ts` and rejects `login.test.ts`.
|
|
14
|
+
*/
|
|
15
|
+
export const filenameMatch = defineRule({
|
|
16
|
+
meta: {
|
|
17
|
+
type: 'problem',
|
|
18
|
+
schema: [{
|
|
19
|
+
type: 'object',
|
|
20
|
+
additionalProperties: false,
|
|
21
|
+
properties: {
|
|
22
|
+
pattern: { type: 'string' },
|
|
23
|
+
message: { type: 'string' },
|
|
24
|
+
flags: { type: 'string' },
|
|
25
|
+
},
|
|
26
|
+
required: ['pattern', 'message'],
|
|
27
|
+
}],
|
|
28
|
+
messages: {
|
|
29
|
+
mismatch: '{{message}}',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
createOnce(context) {
|
|
33
|
+
return {
|
|
34
|
+
Program(program) {
|
|
35
|
+
const { pattern, message, flags } = optionsFirst<FilenameMatchOptions>(context)
|
|
36
|
+
|
|
37
|
+
if (!new RegExp(pattern, flags).test(namingFileBasename(context.filename))) {
|
|
38
|
+
context.report({
|
|
39
|
+
node: program,
|
|
40
|
+
messageId: 'mismatch',
|
|
41
|
+
data: { message },
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { defineRule } from '@oxlint/plugins'
|
|
2
|
+
import {
|
|
3
|
+
namingDirSegments,
|
|
4
|
+
namingFileBasename,
|
|
5
|
+
namingFolderPrefixes,
|
|
6
|
+
namingPosixPath,
|
|
7
|
+
namingSegmentsAfter,
|
|
8
|
+
optionsFirst,
|
|
9
|
+
} from '../utils/index.ts'
|
|
10
|
+
|
|
11
|
+
interface FolderPrefixOptions {
|
|
12
|
+
singularize?: 'none' | 'trailing-s'
|
|
13
|
+
separators?: string[]
|
|
14
|
+
after?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Checks that a filename equals or starts with its parent folder name, or with the folders after a configured root.
|
|
19
|
+
*
|
|
20
|
+
* Example: Inside `users/`, `user-card.tsx` passes. With `after: 'components'`, `components/admin/insights/admin-insights-foo.tsx` passes.
|
|
21
|
+
*/
|
|
22
|
+
export const folderPrefix = defineRule({
|
|
23
|
+
meta: {
|
|
24
|
+
type: 'problem',
|
|
25
|
+
schema: [{
|
|
26
|
+
type: 'object',
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
properties: {
|
|
29
|
+
singularize: { type: 'string', enum: ['none', 'trailing-s'] },
|
|
30
|
+
separators: { type: 'array', items: { type: 'string' } },
|
|
31
|
+
after: { type: 'string' },
|
|
32
|
+
},
|
|
33
|
+
}],
|
|
34
|
+
messages: {
|
|
35
|
+
prefix: "File '{{file}}' must start with folder name '{{folder}}' or an allowed singular form.",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
createOnce(context) {
|
|
39
|
+
return {
|
|
40
|
+
Program(program) {
|
|
41
|
+
const {
|
|
42
|
+
singularize = 'trailing-s',
|
|
43
|
+
separators = ['-'],
|
|
44
|
+
after,
|
|
45
|
+
} = optionsFirst<FolderPrefixOptions>(context, {})
|
|
46
|
+
const filename = namingPosixPath(context.filename)
|
|
47
|
+
const folders = after
|
|
48
|
+
? namingSegmentsAfter(filename, after)
|
|
49
|
+
: [namingDirSegments(filename).at(-1) ?? '']
|
|
50
|
+
|
|
51
|
+
if (!folders) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const resolved = folders.length > 0 ? folders : [namingDirSegments(filename).at(-1) ?? '']
|
|
56
|
+
const separator = separators[0] ?? '-'
|
|
57
|
+
const prefixes = namingFolderPrefixes(resolved, separator, singularize)
|
|
58
|
+
const file = namingFileBasename(filename)
|
|
59
|
+
const stem = file.replace(/\.(tsx?|jsx?)$/, '')
|
|
60
|
+
const valid = prefixes.some((prefix) => stem === prefix
|
|
61
|
+
|| separators.some((item) => stem.startsWith(`${prefix}${item}`)))
|
|
62
|
+
|
|
63
|
+
if (!valid) {
|
|
64
|
+
context.report({
|
|
65
|
+
node: program,
|
|
66
|
+
messageId: 'prefix',
|
|
67
|
+
data: { file, folder: prefixes[0] ?? '' },
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { defineRule } from '@oxlint/plugins'
|
|
2
|
+
import { exportsCollect, namingCamelCase, namingFileBasename, namingPascalCase, optionsFirst } from '../utils/index.ts'
|
|
3
|
+
|
|
4
|
+
interface NoExtraExportsOptions {
|
|
5
|
+
names: string[]
|
|
6
|
+
domainStem?: 'before-first-dot' | 'full-basename'
|
|
7
|
+
allowTypeExports?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Allows only the configured export-name templates, with placeholders derived from the filename.
|
|
12
|
+
*
|
|
13
|
+
* Example: `billing.service.ts` may allow `makeBillingService`; an exported `helper` then fails.
|
|
14
|
+
*/
|
|
15
|
+
export const noExtraExports = defineRule({
|
|
16
|
+
meta: {
|
|
17
|
+
type: 'problem',
|
|
18
|
+
schema: [{
|
|
19
|
+
type: 'object',
|
|
20
|
+
additionalProperties: false,
|
|
21
|
+
properties: {
|
|
22
|
+
names: { type: 'array', items: { type: 'string' } },
|
|
23
|
+
domainStem: { type: 'string', enum: ['before-first-dot', 'full-basename'] },
|
|
24
|
+
allowTypeExports: { type: 'boolean' },
|
|
25
|
+
},
|
|
26
|
+
required: ['names'],
|
|
27
|
+
}],
|
|
28
|
+
messages: {
|
|
29
|
+
extraExport: "Export '{{name}}' is not allowed; expected one of: {{allowed}}.",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
createOnce(context) {
|
|
33
|
+
return {
|
|
34
|
+
Program(program) {
|
|
35
|
+
const options = optionsFirst<NoExtraExportsOptions>(context)
|
|
36
|
+
const basename = namingFileBasename(context.filename).replace(/\.(tsx?|jsx?)$/, '')
|
|
37
|
+
const domain = options.domainStem === 'full-basename' ? basename : (basename.split('.')[0] ?? '')
|
|
38
|
+
const allowed = options.names.map((template) => template
|
|
39
|
+
.replaceAll('{Domain}', namingPascalCase(domain))
|
|
40
|
+
.replaceAll('{domain}', namingCamelCase(domain)))
|
|
41
|
+
|
|
42
|
+
for (const binding of exportsCollect(program)) {
|
|
43
|
+
if (options.allowTypeExports && binding.typeOnly) {
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const name = binding.localName ?? binding.exportedName
|
|
48
|
+
|
|
49
|
+
if (!allowed.includes(name)) {
|
|
50
|
+
context.report({
|
|
51
|
+
node: binding.node,
|
|
52
|
+
messageId: 'extraExport',
|
|
53
|
+
data: { name, allowed: allowed.join(', ') },
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
})
|