@jameswyse/oxc-config 0.1.0 → 0.2.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/README.md +36 -13
- package/dist/format.d.ts +5 -3
- package/dist/lint.d.ts +2 -1
- package/dist/lint.js +29 -10
- package/dist/rules/import-boundaries.js +10 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,6 +23,26 @@ peerDependencyRules:
|
|
|
23
23
|
|
|
24
24
|
`autoInstallPeers` affects every dependency in the workspace. No ESLint configuration is needed either way.
|
|
25
25
|
|
|
26
|
+
## Set up with a coding agent
|
|
27
|
+
|
|
28
|
+
To have a coding agent install and configure the package, give it this prompt:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
Set up @jameswyse/oxc-config in this repository by following its README:
|
|
32
|
+
https://github.com/jameswyse/oxc-config#readme
|
|
33
|
+
|
|
34
|
+
1. Install the package and its peers, then create oxlint.config.ts and oxfmt.config.ts. In a monorepo, check which directory each package's lint script runs from and which config Oxlint finds from there.
|
|
35
|
+
2. Survey the code before choosing options. Pass each option the evidence supports:
|
|
36
|
+
- react, nextjs, node, vitest, jest, effect, electron and tests: the frameworks, runtimes and test runners each package uses, scoped to the files that use them.
|
|
37
|
+
- env: the modules that read process.env or import.meta.env. Each runtime should have one module that parses configuration and passes typed values on. If reads are scattered, list them as owners for now and record the consolidation as follow-up.
|
|
38
|
+
- boundaries: code that must not import from another part of the project, such as browser code importing server modules, Node built-ins or database clients, an Electron renderer importing main-process code, or a shared package importing an application.
|
|
39
|
+
- serverActionGuards: the functions that "use server" actions call to check authentication or permissions.
|
|
40
|
+
- clock and random: the modules that own the current time and ID or random value generation.
|
|
41
|
+
3. Extend @jameswyse/oxc-config/tsconfig unless the project's existing compiler settings conflict with it.
|
|
42
|
+
4. Run the lint and format scripts. Fix the violations. Suppress a rule only where it reports correct code, with the reason after --, and list each such case as a possible upstream issue.
|
|
43
|
+
5. Report every option above, with the value you set or the evidence for leaving it out. Leaving out env or boundaries in an application needs a specific reason.
|
|
44
|
+
```
|
|
45
|
+
|
|
26
46
|
## Configure
|
|
27
47
|
|
|
28
48
|
Create `oxlint.config.ts` in the project root:
|
|
@@ -58,6 +78,8 @@ Add the scripts:
|
|
|
58
78
|
|
|
59
79
|
Errors fail the run. Warnings mark patterns worth reviewing that have legitimate uses, so they report without blocking: dependency cycles, barrel and forwarding modules, `typeof` outside type guards, swallowed errors, boolean parameters on exported functions, pass-through functions, tests without a matching owner file and test timeout overrides. `lint:fix` formats after applying lint fixes, then lints again to confirm the result.
|
|
60
80
|
|
|
81
|
+
The defaults enable no framework profiles or project boundaries, because both depend on the project. Select the profiles below, then [configure the boundaries](#enforce-project-boundaries) that fit.
|
|
82
|
+
|
|
61
83
|
## Select framework and runtime profiles
|
|
62
84
|
|
|
63
85
|
Enable only the tools the project uses. Each profile accepts `true` for its default scope or an array of project-relative globs. `nextjs` takes application root directories instead of globs.
|
|
@@ -81,29 +103,30 @@ export default createLintConfig({
|
|
|
81
103
|
});
|
|
82
104
|
```
|
|
83
105
|
|
|
84
|
-
| Option | Adds
|
|
85
|
-
| ---------- |
|
|
86
|
-
| `react` | React and React Compiler rules, accessibility rules, effect-misuse rules, and e18e performance rules outside tests
|
|
87
|
-
| `nextjs` | The React profile plus Next.js rules, with default exports allowed for `app` and `pages` entrypoints under each root
|
|
88
|
-
| `node` | Node.js environment and rules
|
|
89
|
-
| `vitest` | Vitest rules; its globs join the shared test scope
|
|
90
|
-
| `jest` | Jest rules; keep its scope separate from Vitest when both exist
|
|
91
|
-
| `effect` | Effect error-tag and service-import rules, for modules that use [Effect](https://effect.website)
|
|
92
|
-
| `tests` | Extra test globs, such as Playwright or Node tests, for the custom test rules
|
|
93
|
-
| `electron` | Electron security rules everywhere, and an import boundary keeping `electron` and `node:*` out of the renderer (`src/renderer/**` by default, or `{ renderer: [...] }`) |
|
|
106
|
+
| Option | Adds |
|
|
107
|
+
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
108
|
+
| `react` | React and React Compiler rules, accessibility rules, effect-misuse rules, and e18e performance rules outside tests |
|
|
109
|
+
| `nextjs` | The React profile plus Next.js rules, with default exports allowed for `app` and `pages` entrypoints under each root |
|
|
110
|
+
| `node` | Node.js environment and rules |
|
|
111
|
+
| `vitest` | Vitest rules; its globs join the shared test scope |
|
|
112
|
+
| `jest` | Jest rules; keep its scope separate from Vitest when both exist |
|
|
113
|
+
| `effect` | Effect error-tag and service-import rules, for modules that use [Effect](https://effect.website) |
|
|
114
|
+
| `tests` | Extra test globs, such as Playwright or Node tests, for the custom test rules |
|
|
115
|
+
| `electron` | Electron security rules everywhere, and an import boundary keeping `electron` and `node:*` out of the renderer (`src/renderer/**` by default, or `{ renderer: [...] }`). Needs [`root`](#enforce-project-boundaries) |
|
|
94
116
|
|
|
95
117
|
Default exports are also accepted where a tool loads them: config files, Storybook stories and `.storybook`, `sanity.cli`, GraphQL Codegen entrypoints and declaration files.
|
|
96
118
|
|
|
97
|
-
In a monorepo, run Oxlint from each package with a `tsconfig.json`, or pass an existing lint tsconfig with `--tsconfig`.
|
|
119
|
+
In a monorepo, run Oxlint from each package with a `tsconfig.json`, or pass an existing lint tsconfig with `--tsconfig`. Oxlint uses the nearest config file in or above the directory it runs from, so one root config can serve every package. Write its globs from the root, such as `apps/web/src/**`.
|
|
98
120
|
|
|
99
121
|
## Enforce project boundaries
|
|
100
122
|
|
|
101
|
-
These options need the project to name its owners, so
|
|
123
|
+
These options need the project to name its owners, so the defaults cannot enable them. Most applications have a configuration owner and at least one runtime boundary, so check each option during setup. Like the profiles, their globs are relative to the config file's directory, wherever Oxlint runs from.
|
|
102
124
|
|
|
103
125
|
```ts
|
|
104
126
|
import { createLintConfig } from "@jameswyse/oxc-config/oxlint";
|
|
105
127
|
|
|
106
128
|
export default createLintConfig({
|
|
129
|
+
root: import.meta.dirname,
|
|
107
130
|
react: ["src/renderer/**"],
|
|
108
131
|
env: ["src/main/config.ts", "src/renderer/env.ts"],
|
|
109
132
|
boundaries: [
|
|
@@ -120,7 +143,7 @@ export default createLintConfig({
|
|
|
120
143
|
```
|
|
121
144
|
|
|
122
145
|
- `env` lists the modules that may read `process.env` or `import.meta.env`. Other source files must receive parsed configuration. Config files and scripts are entrypoints and remain allowed.
|
|
123
|
-
- `boundaries` rejects imports from `files` that match a `deny` glob. Bare specifiers match as written. Relative specifiers resolve to a
|
|
146
|
+
- `boundaries` rejects imports from `files` that match a `deny` glob. Bare specifiers match as written. Relative specifiers resolve to a path from the config file's directory first, so `../main/ipc` matches `src/main/**`. Type-only imports are allowed because they are erased. Use `ignore` to exclude files from a boundary's scope. The boundary rule needs `root: import.meta.dirname` to find that directory, and `createLintConfig` throws without it when `boundaries` or `electron` is set.
|
|
124
147
|
- `serverActionGuards` requires every exported function in a `"use server"` module, and every inline `"use server"` function, to start by awaiting one of the named guards. Server-action modules must export each action where it is declared. A guard's name cannot prove it authorises the operation, so review the guard itself.
|
|
125
148
|
- `clock` and `random` list the modules that may read the current time (`Date.now()`, argument-less `new Date()`) or generate random values and IDs (`Math.random()`, `crypto.randomUUID()`, `randomUUID`/`randomInt` from `node:crypto`). Other production code must receive them, so tests can control them without faking globals. Test files, parameter defaults such as `now = new Date()` and passing `Date.now` uncalled are allowed. Durations from `performance.now()` and secret generation are out of scope.
|
|
126
149
|
|
package/dist/format.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type { OxfmtConfig
|
|
2
|
-
|
|
1
|
+
import type { OxfmtConfig } from "oxfmt";
|
|
2
|
+
type SortImportsOptions = Exclude<OxfmtConfig["sortImports"], boolean | undefined>;
|
|
3
|
+
export declare const formatConfig: Omit<OxfmtConfig, "ignorePatterns" | "sortImports"> & {
|
|
3
4
|
ignorePatterns: string[];
|
|
4
|
-
sortImports:
|
|
5
|
+
sortImports: SortImportsOptions;
|
|
5
6
|
};
|
|
7
|
+
export {};
|
package/dist/lint.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DummyRuleMap, OxlintConfig, OxlintOverride } from "oxlint";
|
|
2
2
|
export type FileScope = boolean | string[];
|
|
3
3
|
export interface LintOptions {
|
|
4
|
+
root?: string;
|
|
4
5
|
typeAware?: boolean;
|
|
5
6
|
node?: FileScope;
|
|
6
7
|
react?: FileScope;
|
|
@@ -30,4 +31,4 @@ export type SharedLintConfig = OxlintConfig & {
|
|
|
30
31
|
rules: DummyRuleMap;
|
|
31
32
|
};
|
|
32
33
|
export declare const defaultIgnores: string[];
|
|
33
|
-
export declare function createLintConfig({ typeAware, node, react, nextjs, vitest, jest, effect, tests, env, boundaries, serverActionGuards, clock, random, electron, }?: LintOptions): SharedLintConfig;
|
|
34
|
+
export declare function createLintConfig({ root, typeAware, node, react, nextjs, vitest, jest, effect, tests, env, boundaries, serverActionGuards, clock, random, electron, }?: LintOptions): SharedLintConfig;
|
package/dist/lint.js
CHANGED
|
@@ -255,12 +255,12 @@ const testRuleNames = [
|
|
|
255
255
|
"valid-expect",
|
|
256
256
|
"valid-expect-in-promise",
|
|
257
257
|
];
|
|
258
|
-
export function createLintConfig({ typeAware = true, node = false, react = false, nextjs = false, vitest = false, jest = false, effect = false, tests = [], env, boundaries = [], serverActionGuards, clock, random, electron = false, } = {}) {
|
|
259
|
-
const nextRoots = filesFor(nextjs, ["."]).map((
|
|
260
|
-
const normalized =
|
|
258
|
+
export function createLintConfig({ root, typeAware = true, node = false, react = false, nextjs = false, vitest = false, jest = false, effect = false, tests = [], env, boundaries = [], serverActionGuards, clock, random, electron = false, } = {}) {
|
|
259
|
+
const nextRoots = filesFor(nextjs, ["."]).map((appRoot) => {
|
|
260
|
+
const normalized = appRoot.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
261
261
|
return normalized === "." || normalized === "" ? "" : `${normalized}/`;
|
|
262
262
|
});
|
|
263
|
-
const nextFiles = nextRoots.flatMap((
|
|
263
|
+
const nextFiles = nextRoots.flatMap((appRoot) => sourceFiles.map((pattern) => `${appRoot}${pattern}`));
|
|
264
264
|
const reactFiles = reactScope(react, nextFiles);
|
|
265
265
|
const allTestFiles = [
|
|
266
266
|
...new Set([
|
|
@@ -325,9 +325,12 @@ export function createLintConfig({ typeAware = true, node = false, react = false
|
|
|
325
325
|
]
|
|
326
326
|
: boundaries;
|
|
327
327
|
if (importBoundaries.length > 0) {
|
|
328
|
+
if (root === undefined) {
|
|
329
|
+
throw new TypeError("Import boundaries resolve from the config file, so pass root: import.meta.dirname.");
|
|
330
|
+
}
|
|
328
331
|
overrides.push({
|
|
329
332
|
files: sourceFiles,
|
|
330
|
-
rules: { "wyse/import-boundaries": ["error", { boundaries: importBoundaries }] },
|
|
333
|
+
rules: { "wyse/import-boundaries": ["error", { root, boundaries: importBoundaries }] },
|
|
331
334
|
});
|
|
332
335
|
}
|
|
333
336
|
if (electron) {
|
|
@@ -377,6 +380,22 @@ export function createLintConfig({ typeAware = true, node = false, react = false
|
|
|
377
380
|
"react/exhaustive-deps": "error",
|
|
378
381
|
"react/no-array-index-key": "error",
|
|
379
382
|
"react/react-in-jsx-scope": "off",
|
|
383
|
+
"jsx-a11y/control-has-associated-label": [
|
|
384
|
+
"error",
|
|
385
|
+
{
|
|
386
|
+
ignoreElements: [
|
|
387
|
+
"audio",
|
|
388
|
+
"canvas",
|
|
389
|
+
"embed",
|
|
390
|
+
"input",
|
|
391
|
+
"td",
|
|
392
|
+
"textarea",
|
|
393
|
+
"tr",
|
|
394
|
+
"video",
|
|
395
|
+
],
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
"jsx-a11y/label-has-associated-control": ["error", { depth: 25 }],
|
|
380
399
|
"jsx-a11y/no-noninteractive-tabindex": ["error", { roles: ["region", "tabpanel"] }],
|
|
381
400
|
"jsx-a11y/prefer-tag-over-role": "off",
|
|
382
401
|
},
|
|
@@ -393,11 +412,11 @@ export function createLintConfig({ typeAware = true, node = false, react = false
|
|
|
393
412
|
plugins: [...basePlugins, "react", "jsx-a11y", "nextjs"],
|
|
394
413
|
rules: nextRules,
|
|
395
414
|
}, {
|
|
396
|
-
files: nextRoots.flatMap((
|
|
397
|
-
`${
|
|
398
|
-
`${
|
|
399
|
-
`${
|
|
400
|
-
`${
|
|
415
|
+
files: nextRoots.flatMap((appRoot) => ["", "src/"].flatMap((source) => [
|
|
416
|
+
`${appRoot}${source}app/**/{page,layout,template,loading,error,global-error,not-found,global-not-found,forbidden,unauthorized,default}.{js,jsx,ts,tsx}`,
|
|
417
|
+
`${appRoot}${source}app/**/{sitemap,robots,manifest,icon,apple-icon,opengraph-image,twitter-image}.{js,jsx,ts,tsx}`,
|
|
418
|
+
`${appRoot}${source}pages/**/*.{js,jsx,ts,tsx}`,
|
|
419
|
+
`${appRoot}${source}{middleware,proxy}.{js,ts}`,
|
|
401
420
|
])),
|
|
402
421
|
excludeFiles: allTestFiles,
|
|
403
422
|
rules: { "import/no-default-export": "off" },
|
|
@@ -17,15 +17,16 @@ function parseBoundary(value) {
|
|
|
17
17
|
}
|
|
18
18
|
return { files, ignore, deny, message };
|
|
19
19
|
}
|
|
20
|
-
function
|
|
20
|
+
function parseSettings(options) {
|
|
21
21
|
const [option] = options;
|
|
22
22
|
if (typeof option !== "object" ||
|
|
23
23
|
option === null ||
|
|
24
24
|
Array.isArray(option) ||
|
|
25
|
+
typeof option.root !== "string" ||
|
|
25
26
|
!Array.isArray(option.boundaries)) {
|
|
26
|
-
throw new TypeError("Configure import boundaries with a boundaries array.");
|
|
27
|
+
throw new TypeError("Configure import boundaries with a root and a boundaries array.");
|
|
27
28
|
}
|
|
28
|
-
return option.boundaries.map(parseBoundary);
|
|
29
|
+
return { root: option.root, boundaries: option.boundaries.map(parseBoundary) };
|
|
29
30
|
}
|
|
30
31
|
export const importBoundaries = defineRule({
|
|
31
32
|
meta: {
|
|
@@ -35,6 +36,7 @@ export const importBoundaries = defineRule({
|
|
|
35
36
|
{
|
|
36
37
|
type: "object",
|
|
37
38
|
properties: {
|
|
39
|
+
root: { type: "string" },
|
|
38
40
|
boundaries: {
|
|
39
41
|
type: "array",
|
|
40
42
|
items: {
|
|
@@ -50,15 +52,16 @@ export const importBoundaries = defineRule({
|
|
|
50
52
|
},
|
|
51
53
|
},
|
|
52
54
|
},
|
|
53
|
-
required: ["boundaries"],
|
|
55
|
+
required: ["root", "boundaries"],
|
|
54
56
|
additionalProperties: false,
|
|
55
57
|
},
|
|
56
58
|
],
|
|
57
59
|
messages: { boundary: "Do not import '{{specifier}}' here. {{message}}" },
|
|
58
60
|
},
|
|
59
61
|
create(context) {
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
+
const { root, boundaries } = parseSettings(context.options);
|
|
63
|
+
const file = projectPath(root, context.filename);
|
|
64
|
+
const active = boundaries.filter((boundary) => matchesAny(file, boundary.files) && !matchesAny(file, boundary.ignore));
|
|
62
65
|
if (active.length === 0) {
|
|
63
66
|
return {};
|
|
64
67
|
}
|
|
@@ -68,7 +71,7 @@ export const importBoundaries = defineRule({
|
|
|
68
71
|
}
|
|
69
72
|
const specifier = source.value;
|
|
70
73
|
const target = specifier.startsWith(".")
|
|
71
|
-
? projectPath(
|
|
74
|
+
? projectPath(root, resolvePath(dirname(context.filename), specifier))
|
|
72
75
|
: specifier;
|
|
73
76
|
const boundary = active.find(({ deny }) => matchesAny(target, deny));
|
|
74
77
|
if (boundary) {
|