@lewishowles/lint-config 0.6.0 → 0.7.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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0: 2026-09-30
4
+
5
+ ### Changes
6
+
7
+ - Breaking: `base` now reports an error for imports that reach into a parent folder, such as `../utils`. Use the package's subpath imports or an alias instead; `--fix` can't rewrite these.
8
+ - Breaking: `base` now expects a blank line before an `await` statement that follows other code, and around `const` declarations that span several lines. Run `--fix` once to update existing code.
9
+ - New opt-in `imports.json` layer with formatter settings that sort imports. See the README for how to add it.
10
+
11
+ ## 0.6.1: 2026-09-21
12
+
13
+ ### Fixes
14
+
15
+ - `comments/function-documentation`: accept the name the JSDoc gives a destructured object parameter, instead of always expecting `options`.
16
+
3
17
  ## 0.6.0: 2026-09-21
4
18
 
5
19
  ### Changes
package/README.md CHANGED
@@ -186,15 +186,33 @@ Plugins are additive and deduplicated: your local plugins are added to the share
186
186
 
187
187
  ## Layers
188
188
 
189
- | Layer | File | Contents |
190
- | ---------- | --------------- | -------------------------------------------------------------------------------------- |
191
- | `base` | `base.json` | Correctness and formatting rules, import sorting, `oxc`/`typescript`/`unicorn` plugins |
192
- | `comments` | `comments.json` | Optional comment-formatting rules, variable-declaration documentation, JSDoc checks |
193
- | `vue` | `vue.json` | Extends `base`, adds the `vue` plugin, Vue compiler macro globals, Vue-specific rules |
189
+ | Layer | File | Contents |
190
+ | ---------- | --------------- | ----------------------------------------------------------------------------------------------- |
191
+ | `base` | `base.json` | Correctness and formatting rules, import sorting, `import`/`oxc`/`typescript`/`unicorn` plugins |
192
+ | `comments` | `comments.json` | Optional comment-formatting rules, variable-declaration documentation, JSDoc checks |
193
+ | `vue` | `vue.json` | Extends `base`, adds the `vue` plugin, Vue compiler macro globals, Vue-specific rules |
194
194
 
195
195
  ### Import sorting
196
196
 
197
- The base layer sorts named members within each import statement, but leaves declaration order (which import comes first) to Oxfmt: enable Oxfmt's `sortImports` option in your local `.oxfmtrc.json` if you want that sorted and fixed automatically.
197
+ The base lint layer sorts named members within each import statement. To sort whole import statements, opt in to `imports.json`. It contains Oxfmt settings, not an Oxlint layer, so add it to the `fmt` block in `vite.config.js`:
198
+
199
+ ```js
200
+ import { defineConfig } from "vite-plus";
201
+ import importFormat from "@lewishowles/lint-config/imports.json" with { type: "json" };
202
+ import oxfmtrc from "./.oxfmtrc.json" with { type: "json" };
203
+
204
+ export default defineConfig({
205
+ fmt: { ...oxfmtrc, ...importFormat },
206
+ });
207
+ ```
208
+
209
+ If your `vite.config.js` already has a `lint` block, add `fmt` to the same `defineConfig` call: `defineConfig({ lint, fmt: { ...oxfmtrc, ...importFormat } })`.
210
+
211
+ This puts named imports first, including `import type { … }` and imports with both default and named members. Other default imports come second, along with `import type` default imports and namespace imports (`import * as`). `.vue` imports come last. Oxfmt sorts each group by module path, ignoring letter case, and separates the groups with blank lines. Side-effect imports keep their written order and position, so keep them at the top or bottom of your imports: one written among the other imports stays where it is and splits the group around it.
212
+
213
+ ### Parent-folder imports
214
+
215
+ The base layer reports imports from a parent folder (`../`), with no automatic fix. Same-folder (`./`), `@/` alias and package `#` subpath imports are allowed. After upgrading, any existing `../` imports fail lint until they move to an `@/` alias or, in a package without one, to [package subpath imports](https://nodejs.org/api/packages.html#subpath-imports).
198
216
 
199
217
  ## What stays repo-local
200
218
 
package/base.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "plugins": ["oxc", "typescript", "unicorn"],
2
+ "plugins": ["import", "oxc", "typescript", "unicorn"],
3
3
  "jsPlugins": [
4
4
  "@stylistic/eslint-plugin",
5
5
  {
@@ -25,6 +25,7 @@
25
25
  "no-unexpected-multiline": "error",
26
26
  "no-useless-assignment": "error",
27
27
  "preserve-caught-error": "error",
28
+ "import/no-relative-parent-imports": "error",
28
29
  "sort-imports": [
29
30
  "error",
30
31
  {
@@ -62,7 +63,24 @@
62
63
  { "blankLine": "never", "prev": "const", "next": "const" },
63
64
  { "blankLine": "never", "prev": "let", "next": "let" },
64
65
  { "blankLine": "always", "prev": "multiline-const", "next": "*" },
65
- { "blankLine": "always", "prev": "*", "next": "multiline-const" }
66
+ { "blankLine": "always", "prev": "*", "next": "multiline-const" },
67
+ {
68
+ "blankLine": "always",
69
+ "prev": "*",
70
+ "next": { "selector": "ExpressionStatement[expression.type=\"AwaitExpression\"]" }
71
+ },
72
+ {
73
+ "blankLine": "always",
74
+ "prev": { "selector": "ExpressionStatement[expression.type=\"AwaitExpression\"]" },
75
+ "next": "*"
76
+ },
77
+ {
78
+ "blankLine": "never",
79
+ "prev": { "selector": "ExpressionStatement[expression.type=\"AwaitExpression\"]" },
80
+ "next": { "selector": "ExpressionStatement[expression.type=\"AwaitExpression\"]" }
81
+ },
82
+ { "blankLine": "always", "prev": "multiline-expression", "next": "*" },
83
+ { "blankLine": "always", "prev": "*", "next": "multiline-expression" }
66
84
  ],
67
85
 
68
86
  "vite-plus/prefer-vite-plus-imports": "error"
@@ -1,5 +1,8 @@
1
- import { getDocumentationNode, reportFunctionDocumentation } from "../utils/documentation.js";
2
- import { hasImmediateBlockComment, hasImmediateLineComment } from "../utils/source.js";
1
+ import {
2
+ getDocumentationNode,
3
+ reportFunctionDocumentation,
4
+ } from "#comments/utils/documentation.js";
5
+ import { hasImmediateBlockComment, hasImmediateLineComment } from "#comments/utils/source.js";
3
6
 
4
7
  /**
5
8
  * Create the class-documentation rule.
@@ -1,5 +1,5 @@
1
- import { getDocumentationNode } from "../utils/documentation.js";
2
- import { hasImmediateLineComment } from "../utils/source.js";
1
+ import { getDocumentationNode } from "#comments/utils/documentation.js";
2
+ import { hasImmediateLineComment } from "#comments/utils/source.js";
3
3
 
4
4
  // The built-in APIs that require a preceding comment by default.
5
5
  const builtInApis = new Set([
@@ -5,8 +5,7 @@ import {
5
5
  formatJSDocWrapping,
6
6
  hasTargetJSDocTag,
7
7
  isJSDoc,
8
- } from "../utils/jsdoc.js";
9
-
8
+ } from "#comments/utils/jsdoc.js";
10
9
  import {
11
10
  getCommentNeighbours,
12
11
  getCommentText,
@@ -18,15 +17,14 @@ import {
18
17
  isDirectiveComment,
19
18
  isLeadingComment,
20
19
  replaceMinimalComment,
21
- } from "../utils/source.js";
22
-
20
+ } from "#comments/utils/source.js";
23
21
  import {
24
22
  addTerminalPunctuation,
25
23
  capitaliseSentence,
26
24
  formatSentence,
27
25
  refillCommentLines,
28
26
  wrapWords,
29
- } from "../utils/wrap.js";
27
+ } from "#comments/utils/wrap.js";
30
28
 
31
29
  // The line length this rule wraps comments to.
32
30
  const maximumLineLength = 80;
@@ -207,6 +205,7 @@ function formatOrdinaryBlockComment(sourceCode, comment) {
207
205
  formattedLines[firstProseLine],
208
206
  capitaliseSentence,
209
207
  );
208
+
210
209
  formattedLines[lastProseLine] = formatBlockCommentLine(
211
210
  formattedLines[lastProseLine],
212
211
  addTerminalPunctuation,
@@ -1,4 +1,4 @@
1
- import { isFunctionValue, reportFunctionDocumentation } from "../utils/documentation.js";
1
+ import { isFunctionValue, reportFunctionDocumentation } from "#comments/utils/documentation.js";
2
2
 
3
3
  /**
4
4
  * Return the declaration node that owns the documentation position.
@@ -1,5 +1,5 @@
1
- import { getDocumentationNode, isFunctionValue } from "../utils/documentation.js";
2
- import { hasImmediateLineComment } from "../utils/source.js";
1
+ import { getDocumentationNode, isFunctionValue } from "#comments/utils/documentation.js";
2
+ import { hasImmediateLineComment } from "#comments/utils/source.js";
3
3
 
4
4
  // Declaration kinds that require an immediately preceding line comment.
5
5
  const documentedKinds = new Set(["await using", "const", "let", "using"]);
@@ -1,5 +1,5 @@
1
+ import { isDirectiveComment } from "#comments/utils/source.js";
1
2
  import { existsSync, readFileSync } from "node:fs";
2
- import { isDirectiveComment } from "../utils/source.js";
3
3
 
4
4
  // Captures the opening tag's attributes separately, so the setup attribute can
5
5
  // be tested without the tag being available from Oxlint's extracted AST.
@@ -1,5 +1,9 @@
1
- import { getCommentNeighbours, isDirectiveComment, isLeadingComment } from "../utils/source.js";
2
- import { getObjectArgument, getObjectProperties, isNamedCall } from "../utils/vue-macro.js";
1
+ import {
2
+ getCommentNeighbours,
3
+ isDirectiveComment,
4
+ isLeadingComment,
5
+ } from "#comments/utils/source.js";
6
+ import { getObjectArgument, getObjectProperties, isNamedCall } from "#comments/utils/vue-macro.js";
3
7
 
4
8
  // Matches the single newline and indentation allowed between a comment and an
5
9
  // event.
@@ -1,10 +1,14 @@
1
+ import {
2
+ getCommentNeighbours,
3
+ isDirectiveComment,
4
+ isLeadingComment,
5
+ } from "#comments/utils/source.js";
1
6
  import {
2
7
  getObjectArgument,
3
8
  getObjectProperties,
4
9
  getPropertyName,
5
10
  isNamedCall,
6
- } from "../utils/vue-macro.js";
7
- import { getCommentNeighbours, isDirectiveComment, isLeadingComment } from "../utils/source.js";
11
+ } from "#comments/utils/vue-macro.js";
8
12
 
9
13
  // Matches the single newline and indentation allowed between a comment and a
10
14
  // prop.
@@ -1,12 +1,11 @@
1
1
  import { getJSDocContent, isJSDoc } from "./jsdoc.js";
2
- import { getPropertyName } from "./vue-macro.js";
3
-
4
2
  import {
5
3
  getCommentNeighbours,
6
4
  getCommentText,
7
5
  isDirectiveComment,
8
6
  isLeadingComment,
9
7
  } from "./source.js";
8
+ import { getPropertyName } from "./vue-macro.js";
10
9
 
11
10
  /**
12
11
  * Return the declaration node that owns the documentation position.
@@ -157,11 +156,14 @@ function getObjectPatternPaths(sourceCode, node, parentPath) {
157
156
  * The Oxlint source code object.
158
157
  * @param {object} node
159
158
  * The parameter node.
159
+ * @param {string} [rootPath]
160
+ * The documented name that starts each path for a destructured object
161
+ * parameter. Defaults to options.
160
162
  *
161
163
  * @returns {string[]}
162
164
  * The required JSDoc parameter paths.
163
165
  */
164
- function getParameterPaths(sourceCode, node) {
166
+ function getParameterPaths(sourceCode, node, rootPath = "options") {
165
167
  if (node.type === "Identifier") {
166
168
  return [node.name];
167
169
  }
@@ -171,11 +173,11 @@ function getParameterPaths(sourceCode, node) {
171
173
  }
172
174
 
173
175
  if (node.type === "ObjectPattern") {
174
- return getObjectPatternPaths(sourceCode, node, "options");
176
+ return getObjectPatternPaths(sourceCode, node, rootPath);
175
177
  }
176
178
 
177
179
  if (node.type === "AssignmentPattern") {
178
- return getParameterPaths(sourceCode, node.left);
180
+ return getParameterPaths(sourceCode, node.left, rootPath);
179
181
  }
180
182
 
181
183
  return [];
@@ -189,25 +191,40 @@ function getParameterPaths(sourceCode, node) {
189
191
  * @param {object} comment
190
192
  * The JSDoc comment token.
191
193
  *
192
- * @returns {Set<string>}
193
- * The documented parameter names.
194
+ * @returns {object}
195
+ * An object with names, the set of every documented parameter path, and
196
+ * topLevelNames, the top-level names in the order they are documented.
194
197
  */
195
198
  function getDocumentedParameters(sourceCode, comment) {
196
199
  // Splits the JSDoc block into its individual lines.
197
200
  const content = getJSDocContent(getCommentText(sourceCode, comment));
198
201
  // Collects the parameter paths documented by @param tags.
199
202
  const names = new Set();
203
+ // Collects top-level parameter names in their documented order.
204
+ const topLevelNames = [];
200
205
 
201
206
  for (const line of content) {
202
207
  // Matches an @param tag and captures its documented path.
203
208
  const match = line.trim().match(/^@param(?:\s+\{[^}]+\})?\s+(\[[^\]]+\]|\S+)/);
204
209
 
205
210
  if (match) {
206
- names.add(match[1]);
211
+ // The documented name as written, including optional brackets and
212
+ // any default value.
213
+ const name = match[1];
214
+
215
+ names.add(name);
216
+
217
+ // Removes optional and default-value syntax before checking for a
218
+ // nested path.
219
+ const topLevelName = name.replace(/^\[|\]$/g, "").split("=")[0];
220
+
221
+ if (!topLevelName.includes(".")) {
222
+ topLevelNames.push(topLevelName);
223
+ }
207
224
  }
208
225
  }
209
226
 
210
- return names;
227
+ return { names, topLevelNames };
211
228
  }
212
229
 
213
230
  /**
@@ -306,11 +323,14 @@ export function reportFunctionDocumentation(context, node, functionNode, options
306
323
  }
307
324
 
308
325
  // Reads the parameter paths already documented by @param tags.
309
- const documentedParameters = getDocumentedParameters(context.sourceCode, comment);
326
+ const { names: documentedParameters, topLevelNames } = getDocumentedParameters(
327
+ context.sourceCode,
328
+ comment,
329
+ );
310
330
 
311
331
  // Derives the parameter paths the function actually requires.
312
- const parameterPaths = functionNode.params.flatMap((parameter) =>
313
- getParameterPaths(context.sourceCode, parameter),
332
+ const parameterPaths = functionNode.params.flatMap((parameter, index) =>
333
+ getParameterPaths(context.sourceCode, parameter, topLevelNames[index]),
314
334
  );
315
335
 
316
336
  for (const path of parameterPaths) {
@@ -421,6 +421,7 @@ function formatMixedTags(lines, width, addPunctuation) {
421
421
  rest: match[2].trim(),
422
422
  type: match[1],
423
423
  };
424
+
424
425
  preserveSection = false;
425
426
 
426
427
  result.push(formatTagHeader(currentEntry));
package/imports.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "sortImports": {
3
+ "groups": ["named", "default", "vue"],
4
+ "customGroups": [
5
+ { "groupName": "vue", "elementNamePattern": ["**/*.vue"] },
6
+ { "groupName": "named", "modifiers": ["named"] },
7
+ { "groupName": "default", "modifiers": ["default"] }
8
+ ],
9
+ "newlinesBetween": true,
10
+ "order": "asc",
11
+ "sortSideEffects": false
12
+ }
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lewishowles/lint-config",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Shared oxlint configuration for Lewis Howles projects",
5
5
  "keywords": [
6
6
  "config",
@@ -23,14 +23,19 @@
23
23
  "base.json",
24
24
  "comments",
25
25
  "comments.json",
26
+ "imports.json",
26
27
  "vue.json",
27
28
  "README.md"
28
29
  ],
29
30
  "type": "module",
31
+ "imports": {
32
+ "#comments/*": "./comments/*"
33
+ },
30
34
  "exports": {
31
35
  "./base.json": "./base.json",
32
36
  "./comments.json": "./comments.json",
33
37
  "./comments/plugin": "./comments/plugin.js",
38
+ "./imports.json": "./imports.json",
34
39
  "./vue.json": "./vue.json"
35
40
  },
36
41
  "publishConfig": {