oxlint-frontier-style 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Steve Brand
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,56 @@
1
+ # `oxlint-frontier-style`
2
+
3
+ This is an [oxlint](https://oxc.rs/docs/guide/usage/linter.html) plugin which enforces the code in the Frontier code style.
4
+
5
+ ## Rules
6
+
7
+ Some conventions are cooler than the rules they run against. Rule escape hatches exist for a reason. Use `oxlint-ignore` where appropriate.
8
+
9
+ ### Module-level JSDoc required
10
+
11
+ Each module-level entity must carry a JSDoc describing what it is and why it exists.
12
+
13
+ ### File JSDoc required
14
+
15
+ Each file must start with a JSDoc block with a `@file` declaration describing what that file does and why it exists.
16
+
17
+ ### Preferred declaration order
18
+
19
+ All declarations must follow the following order:
20
+
21
+ 1. `@file` JSDoc (see above)
22
+ 2. imports
23
+ 3. types
24
+ 4. interfaces
25
+ 5. constants
26
+ 6. functions
27
+ 7. file exports (the final `export {}`/`export default XYZ` block)
28
+
29
+ This order apparently makes natural sense to some developers.
30
+
31
+ ### `SCREAMING_SNAKE_CASE` for module-level constants
32
+
33
+ All module-level constants must be spelled in `SCREAMING_SNAKE_CASE`.
34
+
35
+ _Configurable_:
36
+
37
+ - `exceptions` match against literal names (e.g. `logger`, `formatter`, `rule`)
38
+ - `exceptionPatterns` match against RegEx patterns (e.g. `__.*` to except names like `__unused`)
39
+
40
+ ### Exported entities after unexported ones
41
+
42
+ Exported entities carry a heavier semantic weight (they're meant to be relied upon by consumers). This means they should be closer to the bottom within their category.
43
+
44
+ ### Block types must be separate
45
+
46
+ Declarations of the same type (function calls, constants etc.) may be grouped together. Declarations of different types must be separated by an empty line.
47
+
48
+ ## Should I Use It?
49
+
50
+ No.
51
+
52
+ Unless you're of a particular aesthetic preference or are working on a specific set of packages, you don't need it. This is shared config for a narrow set of projects.
53
+
54
+ ## License
55
+
56
+ MIT
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @file Entry point for the plugin.
3
+ */
4
+ import type { Plugin } from "@oxlint/plugins";
5
+ /**
6
+ * Exported plugin.
7
+ */
8
+ declare const PLUGIN: Plugin;
9
+ export default PLUGIN;
package/dist/index.js ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @file Entry point for the plugin.
3
+ */
4
+ import blockTypeSpacing from "./rules/block-type-spacing.js";
5
+ import exportedAfterLocal from "./rules/exported-after-local.js";
6
+ import constScreamingSnake from "./rules/module-const-screaming-snake.js";
7
+ import moduleStructureOrder from "./rules/module-structure-order.js";
8
+ import requireFileJsdoc from "./rules/require-file-jsdoc.js";
9
+ import requireJsdoc from "./rules/require-jsdoc.js";
10
+ /**
11
+ * Exported plugin.
12
+ */
13
+ const PLUGIN = {
14
+ // * `meta.name` defines rule prefix
15
+ // * e.g. `meta.name` `frontier-style` → rule `frontier-style/no-fucking-around`
16
+ meta: { name: "frontier-style" },
17
+ rules: {
18
+ "module-const-screaming-snake": constScreamingSnake,
19
+ "require-file-jsdoc": requireFileJsdoc,
20
+ "require-jsdoc": requireJsdoc,
21
+ "module-structure-order": moduleStructureOrder,
22
+ "exported-after-local": exportedAfterLocal,
23
+ "block-type-spacing": blockTypeSpacing,
24
+ },
25
+ };
26
+ export default PLUGIN;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @file Shareable preset bundling Frontier's built-in and custom rule configuration.
3
+ */
4
+ import type { OxlintConfig } from "oxlint";
5
+ /**
6
+ * The recommended config for the projects using the Frontier code style.
7
+ */
8
+ declare const RECOMMENDED: OxlintConfig;
9
+ export default RECOMMENDED;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @file Shareable preset bundling Frontier's built-in and custom rule configuration.
3
+ */
4
+ /**
5
+ * The recommended config for the projects using the Frontier code style.
6
+ */
7
+ const RECOMMENDED = {
8
+ plugins: ["eslint", "typescript", "import", "oxc", "promise", "unicorn"],
9
+ jsPlugins: ["oxlint-frontier-style"],
10
+ categories: { correctness: "warn" },
11
+ rules: {
12
+ "prefer-const": "error",
13
+ "id-length": [
14
+ "error",
15
+ { checkGeneric: false, exceptionPatterns: ["^_.*"] },
16
+ ],
17
+ "func-style": ["error", "declaration", { allowArrowFunctions: true }],
18
+ "typescript/consistent-type-imports": "error",
19
+ "typescript/consistent-type-definitions": ["error", "interface"],
20
+ "typescript/explicit-function-return-type": "error",
21
+ "typescript/no-explicit-any": "error",
22
+ "typescript/no-duplicate-type-constituents": "off",
23
+ "import/extensions": [
24
+ "error",
25
+ "always",
26
+ { ignorePackages: true, checkTypeImports: true },
27
+ ],
28
+ "frontier-style/block-type-spacing": "error",
29
+ "frontier-style/module-structure-order": "error",
30
+ "frontier-style/exported-after-local": "error",
31
+ "frontier-style/module-const-screaming-snake": "error",
32
+ "frontier-style/require-file-jsdoc": "error",
33
+ "frontier-style/require-jsdoc": "error",
34
+ },
35
+ };
36
+ export default RECOMMENDED;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @file Rule: separate adjacent statements of different block type with exactly one blank line.
3
+ */
4
+ import type { Context, Visitor } from "@oxlint/plugins";
5
+ declare const _default: {
6
+ meta: {
7
+ type: "layout";
8
+ docs: {
9
+ description: string;
10
+ recommended: boolean;
11
+ };
12
+ messages: {
13
+ missingBlankLine: string;
14
+ };
15
+ };
16
+ create(context: Context): Visitor;
17
+ };
18
+ export default _default;
@@ -0,0 +1,208 @@
1
+ /**
2
+ * @file Rule: separate adjacent statements of different block type with exactly one blank line.
3
+ */
4
+ import { asVariableDeclaration, unwrapExport } from "../shared/ast.js";
5
+ /**
6
+ * Statement types treated as a `return` block.
7
+ */
8
+ const RETURN_TYPES = new Set([
9
+ "ReturnStatement",
10
+ "ThrowStatement",
11
+ "ContinueStatement",
12
+ "BreakStatement",
13
+ ]);
14
+ /**
15
+ * Narrows a node to an `ExpressionStatement`.
16
+ * Returns `null` if the node is not one.
17
+ */
18
+ function asExpressionStatement(node) {
19
+ return node.type === "ExpressionStatement" ? node : null;
20
+ }
21
+ /**
22
+ * Narrows a node to an `IfStatement`.
23
+ * Returns `null` if the node is not one.
24
+ */
25
+ function asIfStatement(node) {
26
+ return node.type === "IfStatement" ? node : null;
27
+ }
28
+ /**
29
+ * Counts the fully-blank lines between two nodes or comments.
30
+ */
31
+ function blankLinesBetween(first, second) {
32
+ return second.loc.start.line - first.loc.end.line - 1;
33
+ }
34
+ /**
35
+ * Whether the node spans more than one line.
36
+ */
37
+ function isMultiline(node) {
38
+ return node.loc.start.line !== node.loc.end.line;
39
+ }
40
+ /**
41
+ * Classifies a `const`/`let` declaration, including by whether is spans one line or multiple.
42
+ */
43
+ function classifyVariable(node, kind) {
44
+ if (kind === "const")
45
+ return isMultiline(node) ? "const-multiline" : "const";
46
+ return isMultiline(node) ? "let-multiline" : "let";
47
+ }
48
+ /**
49
+ * Classifies an expression statement by its expression kind.
50
+ */
51
+ function classifyExpression(expression) {
52
+ if (expression.type === "AssignmentExpression")
53
+ return "assign";
54
+ if (expression.type === "CallExpression")
55
+ return "call";
56
+ if (expression.type === "AwaitExpression")
57
+ return "call";
58
+ return "other";
59
+ }
60
+ /**
61
+ * Classifies an `if` statement as a `guard` (single early-exit, no `else`) or a general `conditional`.
62
+ */
63
+ function classifyIf(consequent, alternate) {
64
+ const isGuard = alternate === null &&
65
+ consequent.type !== "BlockStatement" &&
66
+ RETURN_TYPES.has(consequent.type);
67
+ return isGuard ? "guard" : "conditional";
68
+ }
69
+ /**
70
+ * Classifies a statement node into its block type.
71
+ */
72
+ function classifyStatement(node) {
73
+ const target = unwrapExport(node);
74
+ const declaration = asVariableDeclaration(target);
75
+ if (declaration !== null)
76
+ return classifyVariable(declaration, declaration.kind);
77
+ const expressionStatement = asExpressionStatement(target);
78
+ if (expressionStatement !== null)
79
+ return classifyExpression(expressionStatement.expression);
80
+ const ifStatement = asIfStatement(target);
81
+ if (ifStatement !== null)
82
+ return classifyIf(ifStatement.consequent, ifStatement.alternate);
83
+ if (target.type === "SwitchStatement")
84
+ return "conditional";
85
+ if (target.type === "TryStatement")
86
+ return "try";
87
+ if (RETURN_TYPES.has(target.type))
88
+ return "return";
89
+ return "other";
90
+ }
91
+ /**
92
+ * Splits a statement's leading comments into the run attached to it (no blank line between) and the standalone comments that precede that run.
93
+ */
94
+ function splitAttachedComments(leading, statement) {
95
+ let attachedStartLine = statement.loc.start.line;
96
+ let cursorLine = statement.loc.start.line;
97
+ let splitIndex = leading.length;
98
+ for (let index = leading.length - 1; index >= 0; index--) {
99
+ const comment = leading[index];
100
+ if (comment === undefined)
101
+ break;
102
+ if (comment.loc.end.line !== cursorLine - 1)
103
+ break;
104
+ attachedStartLine = comment.loc.start.line;
105
+ cursorLine = comment.loc.start.line;
106
+ splitIndex = index;
107
+ }
108
+ return { attachedStartLine, standalone: leading.slice(0, splitIndex) };
109
+ }
110
+ /**
111
+ * Appends standalone comments to `items` as `comment` blocks, grouping runs separated by a blank line into distinct items.
112
+ */
113
+ function pushCommentItems(items, comments) {
114
+ let group = [];
115
+ function flush() {
116
+ const first = group[0];
117
+ const last = group[group.length - 1];
118
+ if (first === undefined || last === undefined)
119
+ return;
120
+ items.push({
121
+ kind: "comment",
122
+ startLine: first.loc.start.line,
123
+ endLine: last.loc.end.line,
124
+ node: null,
125
+ });
126
+ }
127
+ for (const comment of comments) {
128
+ const previous = group[group.length - 1];
129
+ if (previous !== undefined &&
130
+ blankLinesBetween(previous, comment) > 0) {
131
+ flush();
132
+ group = [];
133
+ }
134
+ group.push(comment);
135
+ }
136
+ flush();
137
+ }
138
+ /**
139
+ * Builds the ordered list of spacing items for a statement body.
140
+ */
141
+ function buildItems(context, body) {
142
+ const items = [];
143
+ for (const statement of body) {
144
+ const leading = context.sourceCode.getCommentsBefore(statement);
145
+ const { attachedStartLine, standalone } = splitAttachedComments(leading, statement);
146
+ pushCommentItems(items, standalone);
147
+ items.push({
148
+ kind: classifyStatement(statement),
149
+ startLine: attachedStartLine,
150
+ endLine: statement.loc.end.line,
151
+ node: statement,
152
+ });
153
+ }
154
+ return items;
155
+ }
156
+ /**
157
+ * Reports adjacent items of different block type that have no blank line between them.
158
+ */
159
+ function checkBody(context, body) {
160
+ const items = buildItems(context, body);
161
+ for (let index = 1; index < items.length; index++) {
162
+ const previous = items[index - 1];
163
+ const current = items[index];
164
+ if (previous === undefined || current === undefined)
165
+ continue;
166
+ if (previous.kind === current.kind)
167
+ continue;
168
+ const gap = current.startLine - previous.endLine - 1;
169
+ if (gap !== 0)
170
+ continue;
171
+ const data = { before: previous.kind, after: current.kind };
172
+ if (current.node === null) {
173
+ context.report({
174
+ loc: { line: current.startLine, column: 0 },
175
+ messageId: "missingBlankLine",
176
+ data,
177
+ });
178
+ continue;
179
+ }
180
+ context.report({
181
+ node: current.node,
182
+ messageId: "missingBlankLine",
183
+ data,
184
+ });
185
+ }
186
+ }
187
+ export default {
188
+ meta: {
189
+ type: "layout",
190
+ docs: {
191
+ description: "Separate adjacent statements of different block type with exactly one blank line.",
192
+ recommended: true,
193
+ },
194
+ messages: {
195
+ missingBlankLine: "Expected a blank line between `{{before}}` and `{{after}}` blocks.",
196
+ },
197
+ },
198
+ create(context) {
199
+ return {
200
+ Program(node) {
201
+ checkBody(context, node.body);
202
+ },
203
+ BlockStatement(node) {
204
+ checkBody(context, node.body);
205
+ },
206
+ };
207
+ },
208
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @file Rule: within a group (`const`/`let`/functions), local declarations must precede exported ones.
3
+ */
4
+ import type { Context, Visitor } from "@oxlint/plugins";
5
+ declare const _default: {
6
+ meta: {
7
+ type: "suggestion";
8
+ docs: {
9
+ description: string;
10
+ recommended: boolean;
11
+ };
12
+ messages: {
13
+ exportBeforeLocal: string;
14
+ };
15
+ };
16
+ create(context: Context): Visitor;
17
+ };
18
+ export default _default;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @file Rule: within a group (`const`/`let`/functions), local declarations must precede exported ones.
3
+ */
4
+ import { asVariableDeclaration, isExportStatement, unwrapExport, } from "../shared/ast.js";
5
+ /**
6
+ * Returns the group key of a declaration (`function`/`const`/`let`), or `null` if it does not belong to a tracked group.
7
+ */
8
+ function groupKeyOf(node) {
9
+ const inner = unwrapExport(node);
10
+ if (inner.type === "FunctionDeclaration")
11
+ return "function";
12
+ const declaration = asVariableDeclaration(inner);
13
+ if (declaration === null)
14
+ return null;
15
+ return declaration.kind === "const" ? "const" : "let";
16
+ }
17
+ /**
18
+ * Reports any local declaration that appears after an exported one within the same group.
19
+ */
20
+ function checkProgram(context, node) {
21
+ const exportedGroups = new Set();
22
+ for (const statement of node.body) {
23
+ const group = groupKeyOf(statement);
24
+ if (group === null)
25
+ continue;
26
+ const exported = isExportStatement(statement);
27
+ if (exported) {
28
+ exportedGroups.add(group);
29
+ continue;
30
+ }
31
+ if (exportedGroups.has(group)) {
32
+ context.report({
33
+ node: statement,
34
+ messageId: "exportBeforeLocal",
35
+ data: { group },
36
+ });
37
+ }
38
+ }
39
+ }
40
+ export default {
41
+ meta: {
42
+ type: "suggestion",
43
+ docs: {
44
+ description: "Require local declarations to precede exported ones within each `const`/`let`/function group.",
45
+ recommended: true,
46
+ },
47
+ messages: {
48
+ exportBeforeLocal: "Non-exported `{{group}}` declarations must come before exported ones.",
49
+ },
50
+ },
51
+ create(context) {
52
+ return {
53
+ Program(node) {
54
+ checkProgram(context, node);
55
+ },
56
+ };
57
+ },
58
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @file Rule: module-level `const` identifiers must be `SCREAMING_SNAKE_CASE`.
3
+ */
4
+ import type { Context, Visitor } from "@oxlint/plugins";
5
+ declare const _default: {
6
+ meta: {
7
+ type: "suggestion";
8
+ docs: {
9
+ description: string;
10
+ recommended: boolean;
11
+ };
12
+ messages: {
13
+ notScreamingSnake: string;
14
+ };
15
+ schema: {
16
+ type: "object";
17
+ properties: {
18
+ exceptions: {
19
+ type: "array";
20
+ items: {
21
+ type: "string";
22
+ };
23
+ };
24
+ exceptionPatterns: {
25
+ type: "array";
26
+ items: {
27
+ type: "string";
28
+ };
29
+ };
30
+ };
31
+ additionalProperties: false;
32
+ }[];
33
+ };
34
+ create(context: Context): Visitor;
35
+ };
36
+ export default _default;
@@ -0,0 +1,144 @@
1
+ /**
2
+ * @file Rule: module-level `const` identifiers must be `SCREAMING_SNAKE_CASE`.
3
+ */
4
+ import { asVariableDeclaration, unwrapExport } from "../shared/ast.js";
5
+ /**
6
+ * Rule identifier, used to prefix option-validation errors.
7
+ */
8
+ const RULE_ID = "module-const-screaming-snake";
9
+ /**
10
+ * Pattern an identifier must match to be considered `SCREAMING_SNAKE_CASE`.
11
+ */
12
+ const SCREAMING_SNAKE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$/;
13
+ /**
14
+ * Initializer node types that exempt a `const` from the naming requirement (components and the like).
15
+ */
16
+ const EXEMPT_INIT_TYPES = new Set([
17
+ "ArrowFunctionExpression",
18
+ "FunctionExpression",
19
+ "ClassExpression",
20
+ ]);
21
+ /**
22
+ * Narrows a node to an `Identifier`.
23
+ * Returns `null` if the node is not one.
24
+ */
25
+ function asIdentifier(node) {
26
+ return node.type === "Identifier" ? node : null;
27
+ }
28
+ /**
29
+ * Whether the declarator's initializer exempts it from the naming requirement.
30
+ */
31
+ function isExemptInitializer(init) {
32
+ return init !== null && EXEMPT_INIT_TYPES.has(init.type);
33
+ }
34
+ /**
35
+ * Whether a value is an array whose every element is a string.
36
+ */
37
+ function isStringArray(value) {
38
+ return (Array.isArray(value) && value.every((item) => typeof item === "string"));
39
+ }
40
+ /**
41
+ * Throws if `pattern` is not a valid regular expression source.
42
+ */
43
+ function assertValidPattern(pattern) {
44
+ try {
45
+ new RegExp(pattern);
46
+ }
47
+ catch (error) {
48
+ throw new SyntaxError(`${RULE_ID}: invalid \`exceptionPatterns\` entry ${JSON.stringify(pattern)}: ${error.message}`);
49
+ }
50
+ }
51
+ /**
52
+ * Parses the raw first config element into typed options.
53
+ *
54
+ * The plugin interface types options as arbitrary JSON, so the shape declared by `meta.schema` is not guaranteed at runtime.
55
+ */
56
+ function parseOptions(raw) {
57
+ if (raw === undefined || raw === null)
58
+ return {};
59
+ if (typeof raw !== "object" || Array.isArray(raw))
60
+ throw new TypeError(`${RULE_ID}: options must be an object, received ${Array.isArray(raw) ? "array" : typeof raw}.`);
61
+ const { exceptions, exceptionPatterns, ...rest } = raw;
62
+ const unknownKeys = Object.keys(rest);
63
+ if (unknownKeys.length > 0)
64
+ throw new TypeError(`${RULE_ID}: unknown option(s): ${unknownKeys.join(", ")}.`);
65
+ if (exceptions !== undefined && !isStringArray(exceptions))
66
+ throw new TypeError(`${RULE_ID}: \`exceptions\` must be an array of strings.`);
67
+ if (exceptionPatterns !== undefined && !isStringArray(exceptionPatterns))
68
+ throw new TypeError(`${RULE_ID}: \`exceptionPatterns\` must be an array of strings.`);
69
+ for (const pattern of exceptionPatterns ?? [])
70
+ assertValidPattern(pattern);
71
+ return { exceptions, exceptionPatterns };
72
+ }
73
+ /**
74
+ * Builds a predicate that reports whether a name is exempt by literal name or matching pattern.
75
+ */
76
+ function makeIsExceptedName(options) {
77
+ const exceptions = new Set(options.exceptions ?? []);
78
+ const patterns = (options.exceptionPatterns ?? []).map((pattern) => new RegExp(pattern));
79
+ return function isExceptedName(name) {
80
+ return (exceptions.has(name) ||
81
+ patterns.some((pattern) => pattern.test(name)));
82
+ };
83
+ }
84
+ /**
85
+ * Reports any module-level `const` identifier that is neither exempt nor `SCREAMING_SNAKE_CASE`.
86
+ */
87
+ function checkStatement(context, statement, isExceptedName) {
88
+ const declaration = asVariableDeclaration(unwrapExport(statement));
89
+ if (declaration === null)
90
+ return;
91
+ if (declaration.kind !== "const")
92
+ return;
93
+ for (const declarator of declaration.declarations) {
94
+ const id = asIdentifier(declarator.id);
95
+ if (id === null)
96
+ continue;
97
+ if (isExemptInitializer(declarator.init))
98
+ continue;
99
+ if (isExceptedName(id.name))
100
+ continue;
101
+ if (SCREAMING_SNAKE.test(id.name))
102
+ continue;
103
+ context.report({
104
+ node: id,
105
+ messageId: "notScreamingSnake",
106
+ data: { name: id.name },
107
+ });
108
+ }
109
+ }
110
+ export default {
111
+ meta: {
112
+ type: "suggestion",
113
+ docs: {
114
+ description: "Require module-level `const` identifiers to be SCREAMING_SNAKE_CASE.",
115
+ recommended: true,
116
+ },
117
+ messages: {
118
+ notScreamingSnake: "Module-level constant `{{name}}` must be SCREAMING_SNAKE_CASE.",
119
+ },
120
+ schema: [
121
+ {
122
+ type: "object",
123
+ properties: {
124
+ exceptions: { type: "array", items: { type: "string" } },
125
+ exceptionPatterns: {
126
+ type: "array",
127
+ items: { type: "string" },
128
+ },
129
+ },
130
+ additionalProperties: false,
131
+ },
132
+ ],
133
+ },
134
+ create(context) {
135
+ const options = parseOptions(context.options[0]);
136
+ const isExceptedName = makeIsExceptedName(options);
137
+ return {
138
+ Program(node) {
139
+ for (const statement of node.body)
140
+ checkStatement(context, statement, isExceptedName);
141
+ },
142
+ };
143
+ },
144
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @file Rule: enforce top-level declaration order.
3
+ */
4
+ import type { Context, Visitor } from "@oxlint/plugins";
5
+ declare const _default: {
6
+ meta: {
7
+ type: "suggestion";
8
+ docs: {
9
+ description: string;
10
+ recommended: boolean;
11
+ };
12
+ messages: {
13
+ outOfOrder: string;
14
+ };
15
+ };
16
+ create(context: Context): Visitor;
17
+ };
18
+ export default _default;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @file Rule: enforce top-level declaration order.
3
+ */
4
+ import { asVariableDeclaration, isExportStatement, unwrapExport, } from "../shared/ast.js";
5
+ /**
6
+ * Human-readable labels for each declaration rank, keyed by descending priority.
7
+ */
8
+ const RANK_LABELS = {
9
+ 1: "Imports",
10
+ 2: "Type aliases",
11
+ 3: "Interfaces",
12
+ 4: "`const` declarations",
13
+ 5: "`let` declarations",
14
+ 6: "Functions",
15
+ 7: "Exports",
16
+ };
17
+ /**
18
+ * Rank of the trailing exports group.
19
+ *
20
+ * TODO: unhardcode
21
+ */
22
+ const RANK_EXPORTS = 7;
23
+ /**
24
+ * Returns rank of a non-export declaration, or `null` if it is not a ranked declaration kind.
25
+ */
26
+ function rankOfDeclaration(node) {
27
+ if (node.type === "ImportDeclaration")
28
+ return 1;
29
+ if (node.type === "TSTypeAliasDeclaration")
30
+ return 2;
31
+ if (node.type === "TSInterfaceDeclaration")
32
+ return 3;
33
+ if (node.type === "FunctionDeclaration")
34
+ return 6;
35
+ const declaration = asVariableDeclaration(node);
36
+ if (declaration === null)
37
+ return null;
38
+ return declaration.kind === "const" ? 4 : 5;
39
+ }
40
+ /**
41
+ * Returns rank of any top-level statement, including exports.
42
+ * Exports unwrap to the rank of their inner declaration, or the trailing exports rank when bare.
43
+ */
44
+ function rankOf(node) {
45
+ if (node.type === "ExportDefaultDeclaration" ||
46
+ node.type === "ExportAllDeclaration")
47
+ return RANK_EXPORTS;
48
+ if (node.type === "ExportNamedDeclaration") {
49
+ const inner = unwrapExport(node);
50
+ // * bare `export { … }` has no inner declaration and is a trailing export
51
+ return inner === node ? RANK_EXPORTS : rankOfDeclaration(inner);
52
+ }
53
+ return rankOfDeclaration(node);
54
+ }
55
+ /**
56
+ * Walks the program body and reports any statement whose rank falls below a rank already seen.
57
+ */
58
+ function checkProgram(context, node) {
59
+ let maxRank = 0;
60
+ for (const statement of node.body) {
61
+ const rank = rankOf(statement);
62
+ if (rank === null)
63
+ continue;
64
+ if (rank < maxRank) {
65
+ const label = isExportStatement(statement) && rank === RANK_EXPORTS
66
+ ? RANK_LABELS[7]
67
+ : RANK_LABELS[rank];
68
+ context.report({
69
+ node: statement,
70
+ messageId: "outOfOrder",
71
+ data: { kind: label ?? "Declaration" },
72
+ });
73
+ continue;
74
+ }
75
+ maxRank = rank;
76
+ }
77
+ }
78
+ export default {
79
+ meta: {
80
+ type: "suggestion",
81
+ docs: {
82
+ description: "Enforce top-level declaration order: imports, types, interfaces, constants, functions, exports.",
83
+ recommended: true,
84
+ },
85
+ messages: {
86
+ outOfOrder: "{{kind}} are out of order. Expected: imports, types, interfaces, const, let, functions, exports.",
87
+ },
88
+ },
89
+ create(context) {
90
+ return {
91
+ Program(node) {
92
+ checkProgram(context, node);
93
+ },
94
+ };
95
+ },
96
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @file Rule: every file must open with a JSDoc block with a `@file` declaration before the first statement.
3
+ */
4
+ import type { Context, Visitor } from "@oxlint/plugins";
5
+ declare const _default: {
6
+ meta: {
7
+ type: "suggestion";
8
+ docs: {
9
+ description: string;
10
+ recommended: boolean;
11
+ };
12
+ messages: {
13
+ missingFileJsdoc: string;
14
+ };
15
+ };
16
+ create(context: Context): Visitor;
17
+ };
18
+ export default _default;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @file Rule: every file must open with a JSDoc block with a `@file` declaration before the first statement.
3
+ */
4
+ /**
5
+ * Whether the comment is a block JSDoc containing an `@file` tag.
6
+ */
7
+ function isFileJsdoc(comment) {
8
+ return (comment.type === "Block" &&
9
+ comment.value.startsWith("*") &&
10
+ comment.value.includes("@file"));
11
+ }
12
+ /**
13
+ * Reports when the file does not open with a `@file` JSDoc block before the first statement.
14
+ */
15
+ function checkProgram(context, node) {
16
+ const comments = context.sourceCode.getAllComments();
17
+ const firstComment = comments.find((comment) => comment.type !== "Shebang");
18
+ const firstStatement = node.body[0];
19
+ // * an empty file has nothing to document
20
+ if (firstStatement === undefined && comments.length === 0)
21
+ return;
22
+ const isBeforeBody = firstComment !== undefined &&
23
+ (firstStatement === undefined ||
24
+ firstComment.range[0] < firstStatement.range[0]);
25
+ if (firstComment !== undefined && isBeforeBody && isFileJsdoc(firstComment))
26
+ return;
27
+ context.report({
28
+ loc: { line: 1, column: 0 },
29
+ messageId: "missingFileJsdoc",
30
+ });
31
+ }
32
+ export default {
33
+ meta: {
34
+ type: "suggestion",
35
+ docs: {
36
+ description: "Require every file to open with a `@file` JSDoc block before the first statement.",
37
+ recommended: true,
38
+ },
39
+ messages: {
40
+ missingFileJsdoc: "File must begin with a `/** … @file … */` JSDoc block.",
41
+ },
42
+ },
43
+ create(context) {
44
+ return {
45
+ Program(node) {
46
+ checkProgram(context, node);
47
+ },
48
+ };
49
+ },
50
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @file Rule: top-level functions, types, interfaces, and `const`s require a preceding block JSDoc.
3
+ */
4
+ import type { Context, Visitor } from "@oxlint/plugins";
5
+ declare const _default: {
6
+ meta: {
7
+ type: "suggestion";
8
+ docs: {
9
+ description: string;
10
+ recommended: boolean;
11
+ };
12
+ messages: {
13
+ missingJsdoc: string;
14
+ };
15
+ };
16
+ create(context: Context): Visitor;
17
+ };
18
+ export default _default;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * @file Rule: top-level functions, types, interfaces, and `const`s require a preceding block JSDoc.
3
+ */
4
+ import { asVariableDeclaration, unwrapExport } from "../shared/ast.js";
5
+ /**
6
+ * Node types that require a preceding block JSDoc.
7
+ */
8
+ const DOCUMENTABLE_TYPES = new Set([
9
+ "FunctionDeclaration",
10
+ "TSTypeAliasDeclaration",
11
+ "TSInterfaceDeclaration",
12
+ ]);
13
+ /**
14
+ * Whether the node is a kind that must carry a block JSDoc.
15
+ */
16
+ function isDocumentable(node) {
17
+ if (DOCUMENTABLE_TYPES.has(node.type))
18
+ return true;
19
+ const declaration = asVariableDeclaration(node);
20
+ return declaration !== null && declaration.kind === "const";
21
+ }
22
+ /**
23
+ * Whether the comment immediately preceding the statement is a JSDoc.
24
+ */
25
+ function hasBlockJsdoc(context, statement) {
26
+ const comments = context.sourceCode.getCommentsBefore(statement);
27
+ const last = comments[comments.length - 1];
28
+ return (last !== undefined &&
29
+ last.type === "Block" &&
30
+ last.value.startsWith("*"));
31
+ }
32
+ /**
33
+ * Returns a human-readable label for the declaration kind.
34
+ */
35
+ function labelFor(node) {
36
+ if (node.type === "FunctionDeclaration")
37
+ return "Function";
38
+ if (node.type === "TSTypeAliasDeclaration")
39
+ return "Type";
40
+ if (node.type === "TSInterfaceDeclaration")
41
+ return "Interface";
42
+ return "Constant";
43
+ }
44
+ /**
45
+ * Reports a documentable statement that lacks a preceding block JSDoc.
46
+ */
47
+ function checkStatement(context, statement) {
48
+ const target = unwrapExport(statement);
49
+ if (!isDocumentable(target))
50
+ return;
51
+ if (hasBlockJsdoc(context, statement))
52
+ return;
53
+ context.report({
54
+ node: target,
55
+ messageId: "missingJsdoc",
56
+ data: { kind: labelFor(target) },
57
+ });
58
+ }
59
+ export default {
60
+ meta: {
61
+ type: "suggestion",
62
+ docs: {
63
+ description: "Require a preceding block JSDoc on top-level functions, types, interfaces, and `const`s.",
64
+ recommended: true,
65
+ },
66
+ messages: {
67
+ missingJsdoc: "{{kind}} declaration must have a preceding block JSDoc (`/** … */`).",
68
+ },
69
+ },
70
+ create(context) {
71
+ return {
72
+ Program(node) {
73
+ for (const statement of node.body) {
74
+ checkStatement(context, statement);
75
+ }
76
+ },
77
+ };
78
+ },
79
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @file Shared AST helpers used by multiple rules.
3
+ */
4
+ import type { ESTree } from "@oxlint/plugins";
5
+ /**
6
+ * Narrows a node to a `VariableDeclaration`, or `null` if it is not one.
7
+ */
8
+ export declare function asVariableDeclaration(node: ESTree.Node): ESTree.VariableDeclaration | null;
9
+ /**
10
+ * `true` if the node is any `export …` statement.
11
+ */
12
+ export declare function isExportStatement(node: ESTree.Node): boolean;
13
+ /**
14
+ * Unwrap an `export const`/`export function`/etc. to its inner declaration.
15
+ *
16
+ * Returns the node unchanged if it is not an inline-exported declaration.
17
+ */
18
+ export declare function unwrapExport(node: ESTree.Node): ESTree.Node;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @file Shared AST helpers used by multiple rules.
3
+ */
4
+ /**
5
+ * Narrows a node to a `VariableDeclaration`, or `null` if it is not one.
6
+ */
7
+ export function asVariableDeclaration(node) {
8
+ return node.type === "VariableDeclaration" ? node : null;
9
+ }
10
+ /**
11
+ * `true` if the node is any `export …` statement.
12
+ */
13
+ export function isExportStatement(node) {
14
+ return node.type.startsWith("Export");
15
+ }
16
+ /**
17
+ * Unwrap an `export const`/`export function`/etc. to its inner declaration.
18
+ *
19
+ * Returns the node unchanged if it is not an inline-exported declaration.
20
+ */
21
+ export function unwrapExport(node) {
22
+ if (node.type !== "ExportNamedDeclaration")
23
+ return node;
24
+ return node.declaration ?? node;
25
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "oxlint-frontier-style",
3
+ "version": "1.0.0",
4
+ "description": "oxlint plugin enforcing the Frontier code style",
5
+ "keywords": [
6
+ "oxlint",
7
+ "plugin"
8
+ ],
9
+ "homepage": "https://github.com/Hyperseeker/oxlint-frontier-style",
10
+ "bugs": {
11
+ "url": "https://github.com/Hyperseeker/oxlint-frontier-style/issues"
12
+ },
13
+ "license": "MIT",
14
+ "repository": "github:Hyperseeker/oxlint-frontier-style",
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "type": "module",
19
+ "main": "dist/index.js",
20
+ "exports": {
21
+ ".": "./dist/index.js",
22
+ "./recommended": "./dist/recommended.js"
23
+ },
24
+ "scripts": {
25
+ "build": "tsgo -b",
26
+ "typecheck": "tsgo --noEmit",
27
+ "lint": "oxlint",
28
+ "format": "oxfmt",
29
+ "format:check": "oxfmt --check",
30
+ "ci": "bun run typecheck && bun run lint && bun run format:check",
31
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
32
+ "prepublishOnly": "bun run ci && bun run clean && bun run build"
33
+ },
34
+ "devDependencies": {
35
+ "@oxlint/plugins": "^1.48.0",
36
+ "@types/node": "^25.9.1",
37
+ "oxfmt": "^0.54.0",
38
+ "oxlint": "^1.48.0",
39
+ "oxlint-tsgolint": "^0.23.0"
40
+ }
41
+ }