@zgeoff/oxlint-config 0.2.7 → 0.2.9
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 +32 -8
- package/import-zones.js +38 -1
- package/no-restricted-words.js +61 -0
- package/package.json +2 -1
- package/plugin.js +2 -0
- package/resolve-import-target.js +8 -1
package/README.md
CHANGED
|
@@ -8,7 +8,8 @@ await arguments, awaits hidden inside a control-flow condition or a `&&`/`||`/`?
|
|
|
8
8
|
`switch` statement, every JSDoc block, any run of more than three line comments, and a repo config
|
|
9
9
|
that loosens either comment rule. `zgeoff/function-verb` enforces the function-naming taxonomy from
|
|
10
10
|
the shared agent guidelines, `zgeoff/test-beside-module` keeps each test file beside the module it
|
|
11
|
-
tests, `zgeoff/import-zones` fails an import that crosses a module boundary the repo declares,
|
|
11
|
+
tests, `zgeoff/import-zones` fails an import that crosses a module boundary the repo declares,
|
|
12
|
+
`zgeoff/no-restricted-words` keeps the words in its `words` option out of a path's text, and
|
|
12
13
|
`zgeoff/no-file-snapshot` bans file snapshots.
|
|
13
14
|
|
|
14
15
|
## Usage
|
|
@@ -86,6 +87,25 @@ first statement, so put the comment above it:
|
|
|
86
87
|
A second test file for a module (`<name>.host.test.ts`) needs its own `<name>.host.<ext>`, so the
|
|
87
88
|
rule reports it beside only `<name>.ts`.
|
|
88
89
|
|
|
90
|
+
## `zgeoff/no-restricted-words`
|
|
91
|
+
|
|
92
|
+
Off unless a repo configures it. The `words` option lists the words to keep out of the text of a
|
|
93
|
+
file: string literals, template text and comments. A word matches whole and without case, so `imp`
|
|
94
|
+
reports `'IMP daemon'` and `'imp-e2e'` but not `'impl'` or `'imp_name'`. Identifiers are code, not
|
|
95
|
+
text, and the rule never reads them. Scope the rule to a path with `overrides`, and turn it off for
|
|
96
|
+
the paths that may name the words:
|
|
97
|
+
|
|
98
|
+
```jsonc
|
|
99
|
+
{
|
|
100
|
+
"rules": {
|
|
101
|
+
"zgeoff/no-restricted-words": ["error", { "words": ["imp", "impd"] }],
|
|
102
|
+
},
|
|
103
|
+
"overrides": [
|
|
104
|
+
{ "files": ["packages/execution/**"], "rules": { "zgeoff/no-restricted-words": "off" } },
|
|
105
|
+
],
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
89
109
|
## `zgeoff/no-file-snapshot`
|
|
90
110
|
|
|
91
111
|
Snapshots are inline only, so the golden value sits in the test that pins it. The rule reports every
|
|
@@ -102,17 +122,21 @@ the boundaries between them. A file belongs to every zone its path matches, so z
|
|
|
102
122
|
Each entry has a `name`, which starts its report, an optional `reason`, which ends it, and optional
|
|
103
123
|
`from` zones and `ignore` globs that limit which importing files it judges.
|
|
104
124
|
|
|
105
|
-
- `edges` judge an import that resolves to a file: relative,
|
|
106
|
-
through its `exports` under `conditions` plus
|
|
107
|
-
|
|
108
|
-
target in
|
|
109
|
-
must match), and `allowWithin: "src/*"` exempts an
|
|
125
|
+
- `edges` judge an import that resolves to a file: relative, with any bundler query such as `?url`
|
|
126
|
+
dropped, or the package's own name resolved through its `exports` under `conditions` plus
|
|
127
|
+
`default`. A target in a `to` zone fails unless it is also in an `allow` zone; with `allow` alone,
|
|
128
|
+
a target in none of the `allow` zones fails, a target in no zone included. `allowTypeOnly` exempts
|
|
129
|
+
type-only imports (`true`, or globs the target must match), and `allowWithin: "src/*"` exempts an
|
|
130
|
+
import inside the importer's own `src/<name>/`.
|
|
110
131
|
- `modules` restrict a package or builtin (`node:` prefix optional, subpaths included) to the
|
|
111
|
-
`allow` zones; `allowTypeOnly: true` exempts type-only imports.
|
|
132
|
+
`allow` zones; `allowTypeOnly: true` exempts type-only imports. A name that ends in `*` restricts
|
|
133
|
+
every specifier that starts with the text before it, such as `@scope/*`, and `node:*` restricts
|
|
134
|
+
every builtin.
|
|
112
135
|
- `globals` restrict a call to a global, bare or through `globalThis`, `self` or `window`.
|
|
113
136
|
- `unzoned` reports a file in a `from` zone that belongs to none of `zones`.
|
|
114
137
|
- `unresolved` reports a relative or self-referencing import that resolves to no file, and a dynamic
|
|
115
|
-
`import()` or `require()` whose specifier is computed.
|
|
138
|
+
`import()`, `require()` or `import.meta.require()` whose specifier is computed. Bun's
|
|
139
|
+
`import.meta.require` used as a value counts as computed too.
|
|
116
140
|
|
|
117
141
|
```jsonc
|
|
118
142
|
{
|
package/import-zones.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isBuiltin } from 'node:module';
|
|
1
2
|
import { relative, sep } from 'node:path';
|
|
2
3
|
import findRepoRoot from './find-repo-root.js';
|
|
3
4
|
import importZonesSchema from './import-zones-schema.js';
|
|
@@ -114,12 +115,25 @@ function isRestrictedModule(entry, specifier) {
|
|
|
114
115
|
const name = toModuleName(specifier);
|
|
115
116
|
|
|
116
117
|
return entry.modules.some((module) => {
|
|
118
|
+
if (module.endsWith('*')) {
|
|
119
|
+
return isPrefixMatch(module.slice(0, -1), specifier);
|
|
120
|
+
}
|
|
121
|
+
|
|
117
122
|
const restricted = toModuleName(module);
|
|
118
123
|
|
|
119
124
|
return name === restricted || name.startsWith(`${restricted}/`);
|
|
120
125
|
});
|
|
121
126
|
}
|
|
122
127
|
|
|
128
|
+
// `node:*` names every builtin, which a specifier may write without the prefix
|
|
129
|
+
function isPrefixMatch(prefix, specifier) {
|
|
130
|
+
if (specifier.startsWith(prefix)) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return prefix === 'node:' && isBuiltin(specifier);
|
|
135
|
+
}
|
|
136
|
+
|
|
123
137
|
function toModuleName(specifier) {
|
|
124
138
|
return specifier.replace(/^node:/u, '');
|
|
125
139
|
}
|
|
@@ -235,6 +249,18 @@ function isTypeOnlyExport(node) {
|
|
|
235
249
|
);
|
|
236
250
|
}
|
|
237
251
|
|
|
252
|
+
function isImportMetaRequire(node) {
|
|
253
|
+
return (
|
|
254
|
+
node.type === 'MemberExpression' &&
|
|
255
|
+
!node.computed &&
|
|
256
|
+
node.object.type === 'MetaProperty' &&
|
|
257
|
+
node.object.meta.name === 'import' &&
|
|
258
|
+
node.object.property.name === 'meta' &&
|
|
259
|
+
node.property.type === 'Identifier' &&
|
|
260
|
+
node.property.name === 'require'
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
238
264
|
const importZones = {
|
|
239
265
|
meta: {
|
|
240
266
|
type: 'problem',
|
|
@@ -284,10 +310,21 @@ const importZones = {
|
|
|
284
310
|
CallExpression(node) {
|
|
285
311
|
checkGlobalCall(file, node);
|
|
286
312
|
|
|
287
|
-
if (
|
|
313
|
+
if (
|
|
314
|
+
(node.callee.type === 'Identifier' && node.callee.name === 'require') ||
|
|
315
|
+
isImportMetaRequire(node.callee)
|
|
316
|
+
) {
|
|
288
317
|
checkImport(file, { node, source: node.arguments[0], typeOnly: false });
|
|
289
318
|
}
|
|
290
319
|
},
|
|
320
|
+
|
|
321
|
+
// Bun's import.meta.require used as a value, such as `const load =
|
|
322
|
+
// import.meta.require`, loads modules no specifier names
|
|
323
|
+
MemberExpression(node) {
|
|
324
|
+
if (isImportMetaRequire(node) && node.parent?.callee !== node) {
|
|
325
|
+
emitUnresolved(file, node, { messageId: 'computed', data: {} });
|
|
326
|
+
}
|
|
327
|
+
},
|
|
291
328
|
};
|
|
292
329
|
},
|
|
293
330
|
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const noRestrictedWords = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'problem',
|
|
4
|
+
messages: {
|
|
5
|
+
restricted:
|
|
6
|
+
'Remove `{{word}}` from this {{place}}: the repo config restricts the word in this path.',
|
|
7
|
+
},
|
|
8
|
+
schema: [
|
|
9
|
+
{
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
words: { type: 'array', items: { type: 'string', minLength: 1 }, uniqueItems: true },
|
|
13
|
+
},
|
|
14
|
+
additionalProperties: false,
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
const words = context.options[0]?.words ?? [];
|
|
20
|
+
|
|
21
|
+
if (words.length === 0) {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const pattern = buildWordPattern(words);
|
|
26
|
+
|
|
27
|
+
const checkText = (node, text, place) => {
|
|
28
|
+
for (const match of text.matchAll(pattern)) {
|
|
29
|
+
context.report({ node, messageId: 'restricted', data: { word: match[0], place } });
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
Literal(node) {
|
|
35
|
+
if (typeof node.value === 'string') {
|
|
36
|
+
checkText(node, node.value, 'string');
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
TemplateElement(node) {
|
|
40
|
+
checkText(node, node.value.cooked ?? node.value.raw, 'template');
|
|
41
|
+
},
|
|
42
|
+
Program() {
|
|
43
|
+
for (const comment of context.sourceCode.getAllComments()) {
|
|
44
|
+
checkText(comment, comment.value, 'comment');
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// a letter, digit or underscore on either side makes the match part of a
|
|
52
|
+
// longer word (`impl`, `simple`), so only those characters bound it
|
|
53
|
+
function buildWordPattern(words) {
|
|
54
|
+
const alternatives = words.map((word) =>
|
|
55
|
+
word.replaceAll(/[$()*+.?[\\\]^{|}]/gu, String.raw`\$&`),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return new RegExp(`(?<![\\p{L}\\p{N}_])(?:${alternatives.join('|')})(?![\\p{L}\\p{N}_])`, 'giu');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default noRestrictedWords;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zgeoff/oxlint-config",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "Shareable oxlint config: strict categories, house-style rules, and a custom JS plugin",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"oxlintrc.json",
|
|
13
13
|
"plugin.js",
|
|
14
14
|
"function-verb.js",
|
|
15
|
+
"no-restricted-words.js",
|
|
15
16
|
"max-consecutive-line-comments.js",
|
|
16
17
|
"no-jsdoc.js",
|
|
17
18
|
"no-comment-rule-override.js",
|
package/plugin.js
CHANGED
|
@@ -4,6 +4,7 @@ import maxConsecutiveLineComments from './max-consecutive-line-comments.js';
|
|
|
4
4
|
import noCommentRuleOverride from './no-comment-rule-override.js';
|
|
5
5
|
import noFileSnapshot from './no-file-snapshot.js';
|
|
6
6
|
import noJSDoc from './no-jsdoc.js';
|
|
7
|
+
import noRestrictedWords from './no-restricted-words.js';
|
|
7
8
|
import testBesideModule from './test-beside-module.js';
|
|
8
9
|
|
|
9
10
|
// custom rules loaded through oxlint's jsPlugins (ESLint v9 rule API): each
|
|
@@ -105,6 +106,7 @@ const plugin = {
|
|
|
105
106
|
'max-consecutive-line-comments': maxConsecutiveLineComments,
|
|
106
107
|
'no-comment-rule-override': noCommentRuleOverride,
|
|
107
108
|
'function-verb': functionVerb,
|
|
109
|
+
'no-restricted-words': noRestrictedWords,
|
|
108
110
|
'test-beside-module': testBesideModule,
|
|
109
111
|
'no-file-snapshot': noFileSnapshot,
|
|
110
112
|
'import-zones': importZones,
|
package/resolve-import-target.js
CHANGED
|
@@ -9,7 +9,7 @@ const sourceExtensions = { '.js': ['.ts', '.tsx'], '.mjs': ['.mts'], '.cjs': ['.
|
|
|
9
9
|
// own name resolves through its `exports`, and `#` names through its `imports`.
|
|
10
10
|
export default function resolveImportTarget(filename, specifier, settings) {
|
|
11
11
|
if (isPathSpecifier(specifier)) {
|
|
12
|
-
return toTarget(findImportedFile(filename, specifier, settings.extensions));
|
|
12
|
+
return toTarget(findImportedFile(filename, toFileSpecifier(specifier), settings.extensions));
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const manifest = findPackageManifest(dirname(filename), settings.root);
|
|
@@ -40,6 +40,13 @@ function resolveManifestTarget(manifest, specifier, conditions) {
|
|
|
40
40
|
return { kind: 'module', specifier };
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// a bundler query such as Vite's `./styles.css?url` names the file before it
|
|
44
|
+
function toFileSpecifier(specifier) {
|
|
45
|
+
const query = specifier.indexOf('?');
|
|
46
|
+
|
|
47
|
+
return query === -1 ? specifier : specifier.slice(0, query);
|
|
48
|
+
}
|
|
49
|
+
|
|
43
50
|
function toTarget(path) {
|
|
44
51
|
return path === null ? { kind: 'missing' } : { kind: 'file', path };
|
|
45
52
|
}
|