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.
Files changed (43) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +101 -0
  4. package/package.json +64 -0
  5. package/src/index.ts +57 -0
  6. package/src/rules/export-file-prefix.ts +70 -0
  7. package/src/rules/export-name-pattern.ts +71 -0
  8. package/src/rules/filename-export-name.ts +86 -0
  9. package/src/rules/filename-match.ts +47 -0
  10. package/src/rules/folder-prefix.ts +73 -0
  11. package/src/rules/no-extra-exports.ts +60 -0
  12. package/src/rules/no-extra-factory-keys.ts +58 -0
  13. package/src/rules/no-file-level-helpers.ts +58 -0
  14. package/src/rules/no-imported-type-alias.ts +83 -0
  15. package/src/rules/no-inline-schema-elements.ts +70 -0
  16. package/src/rules/no-local-schema-construction.ts +162 -0
  17. package/src/rules/no-rederive-schema.ts +66 -0
  18. package/src/rules/no-restricted-files.ts +40 -0
  19. package/src/rules/no-restricted-token.ts +71 -0
  20. package/src/rules/no-runtime-in-types.ts +72 -0
  21. package/src/rules/no-single-use-scalar-schema.ts +164 -0
  22. package/src/rules/no-top-level-functions.ts +77 -0
  23. package/src/rules/no-trivial-functions.ts +56 -0
  24. package/src/rules/no-unescaped-like.ts +67 -0
  25. package/src/rules/only-export-components.ts +78 -0
  26. package/src/rules/require-file-factory.ts +67 -0
  27. package/src/rules/require-object-params.ts +32 -0
  28. package/src/rules/require-orpc-output.ts +64 -0
  29. package/src/rules/require-paired-call.ts +71 -0
  30. package/src/rules/route-surface.ts +62 -0
  31. package/src/utils/helpers/ast.ts +101 -0
  32. package/src/utils/helpers/declarations.ts +203 -0
  33. package/src/utils/helpers/exports.ts +158 -0
  34. package/src/utils/helpers/factories.ts +67 -0
  35. package/src/utils/helpers/index.ts +11 -0
  36. package/src/utils/helpers/naming.ts +89 -0
  37. package/src/utils/helpers/options.ts +5 -0
  38. package/src/utils/helpers/orpc.ts +31 -0
  39. package/src/utils/helpers/react-components.ts +146 -0
  40. package/src/utils/helpers/routes.ts +31 -0
  41. package/src/utils/helpers/sanitizers.ts +33 -0
  42. package/src/utils/helpers/schemas.ts +82 -0
  43. 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
+ })