shoe-box-oxlint-plugins 0.0.0-stage → 0.1.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 +31 -2
- package/dist/no-backdrop-filter.d.mts +12 -0
- package/dist/no-backdrop-filter.d.mts.map +1 -0
- package/dist/no-backdrop-filter.mjs +49 -0
- package/dist/no-backdrop-filter.mjs.map +1 -0
- package/dist/no-cross-package-relative-imports.d.mts +14 -0
- package/dist/no-cross-package-relative-imports.d.mts.map +1 -0
- package/dist/no-cross-package-relative-imports.mjs +69 -0
- package/dist/no-cross-package-relative-imports.mjs.map +1 -0
- package/dist/no-raw-env-access.d.mts +17 -0
- package/dist/no-raw-env-access.d.mts.map +1 -0
- package/dist/no-raw-env-access.mjs +83 -0
- package/dist/no-raw-env-access.mjs.map +1 -0
- package/dist/no-render-time-match-route.d.mts +56 -0
- package/dist/no-render-time-match-route.d.mts.map +1 -0
- package/dist/no-render-time-match-route.mjs +146 -0
- package/dist/no-render-time-match-route.mjs.map +1 -0
- package/dist/no-unexplained-dynamic-import.d.mts +3 -0
- package/dist/no-unexplained-dynamic-import.d.mts.map +1 -0
- package/dist/no-unexplained-dynamic-import.mjs +34 -0
- package/dist/no-unexplained-dynamic-import.mjs.map +1 -0
- package/dist/package-root-DzPJ3Pq6.mjs +39 -0
- package/dist/package-root-DzPJ3Pq6.mjs.map +1 -0
- package/dist/require-disable-description.d.mts +3 -0
- package/dist/require-disable-description.d.mts.map +1 -0
- package/dist/require-disable-description.mjs +40 -0
- package/dist/require-disable-description.mjs.map +1 -0
- package/dist/vendor/anti-slop/effect/index.d.mts +4 -0
- package/dist/vendor/anti-slop/effect/index.d.mts.map +1 -0
- package/dist/vendor/anti-slop/effect/index.mjs +204 -0
- package/dist/vendor/anti-slop/effect/index.mjs.map +1 -0
- package/dist/vendor/anti-slop/index.d.mts +4 -0
- package/dist/vendor/anti-slop/index.d.mts.map +1 -0
- package/dist/vendor/anti-slop/index.mjs +2385 -0
- package/dist/vendor/anti-slop/index.mjs.map +1 -0
- package/package.json +107 -4
- package/src/internal/package-root.ts +52 -0
- package/src/no-backdrop-filter.ts +59 -0
- package/src/no-cross-package-relative-imports.ts +100 -0
- package/src/no-raw-env-access.ts +125 -0
- package/src/no-render-time-match-route.ts +218 -0
- package/src/no-unexplained-dynamic-import.ts +64 -0
- package/src/require-disable-description.ts +63 -0
- package/src/vendor/anti-slop/effect/index.ts +21 -0
- package/src/vendor/anti-slop/effect/rules/no-manual-effect-error-tag.ts +52 -0
- package/src/vendor/anti-slop/effect/rules/no-manual-tag-comparison.ts +45 -0
- package/src/vendor/anti-slop/effect/rules/no-manual-tagged-construction.ts +37 -0
- package/src/vendor/anti-slop/effect/rules/no-service-constructor-imports.ts +52 -0
- package/src/vendor/anti-slop/effect/rules/prefer-effect-match.ts +54 -0
- package/src/vendor/anti-slop/effect/shared/tagged-values.ts +97 -0
- package/src/vendor/anti-slop/index.ts +47 -0
- package/src/vendor/anti-slop/rules/no-array-filter-map.ts +28 -0
- package/src/vendor/anti-slop/rules/no-chained-type-assertions.ts +77 -0
- package/src/vendor/anti-slop/rules/no-conditional-empty-object-spread.ts +49 -0
- package/src/vendor/anti-slop/rules/no-known-value-widening.ts +415 -0
- package/src/vendor/anti-slop/rules/no-module-mocking.ts +80 -0
- package/src/vendor/anti-slop/rules/no-object-parameters.ts +83 -0
- package/src/vendor/anti-slop/rules/no-reduce-accumulator-copy.ts +109 -0
- package/src/vendor/anti-slop/rules/no-reflect-apply.ts +28 -0
- package/src/vendor/anti-slop/rules/no-reflect-get.ts +28 -0
- package/src/vendor/anti-slop/rules/no-runtime-typeof.ts +77 -0
- package/src/vendor/anti-slop/rules/no-shape-in-symbol-names.ts +46 -0
- package/src/vendor/anti-slop/rules/no-unknown-parameters.ts +69 -0
- package/src/vendor/anti-slop/rules/no-unknown-returns.ts +82 -0
- package/src/vendor/anti-slop/rules/no-unknown-type-aliases.ts +54 -0
- package/src/vendor/anti-slop/rules/no-unsafe-dictionary-type.ts +154 -0
- package/src/vendor/anti-slop/rules/no-widen-then-assert.ts +366 -0
- package/src/vendor/anti-slop/rules/require-readable-spacing.ts +47 -0
- package/src/vendor/anti-slop/rules/require-safety-comment-for-type-assertion.ts +131 -0
- package/src/vendor/anti-slop/shared/array-method.ts +94 -0
- package/src/vendor/anti-slop/shared/dictionary-types.ts +515 -0
- package/src/vendor/anti-slop/shared/function-parameters.ts +49 -0
- package/src/vendor/anti-slop/shared/lexical-type-parameters.ts +61 -0
- package/src/vendor/anti-slop/shared/reflect-method.ts +24 -0
- package/src/vendor/anti-slop/shared/scope.ts +15 -0
- package/src/vendor/anti-slop/shared/type-alias-resolution.ts +250 -0
- package/src/vendor/anti-slop/vendor/eslint-stylistic/padding-line-ast.ts +51 -0
- package/src/vendor/anti-slop/vendor/eslint-stylistic/padding-line-between-statements.ts +906 -0
- package/src/vendor/anti-slop/vendor/eslint-stylistic/padding-line-options.d.ts +87 -0
package/README.md
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
|
-
#
|
|
1
|
+
# shoe-box-oxlint-plugins
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Generic [oxlint JS plugins](https://oxc.rs/docs/guide/usage/linter/js-plugins.html) shared by every shoebox repo, plus a vendored copy of [anti-slop](https://github.com/dmmulroy/anti-slop). You normally do not list these yourself: [`shoe-box-oxlint-config`](../oxlint-config) resolves and enables them.
|
|
4
|
+
|
|
5
|
+
Each subpath export is one plugin. In the workspace, exports point at `src/*.ts` (oxlint type-strips them under Node); the published package points at `dist/*.mjs` built by tsdown, because Node refuses to type-strip files under `node_modules`.
|
|
6
|
+
|
|
7
|
+
## Custom rules
|
|
8
|
+
|
|
9
|
+
| Rule | Export | Why |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| `disable-comments/require-description` | `./require-disable-description` | Every `eslint-disable`/`oxlint-disable` directive must carry a `-- reason`; an empty reason after `--` does not count. Also forbids `@effect-diagnostics` comments, which silence tsgolint but escape `reportUnusedDisableDirectives`, so Effect rules are suppressed with `oxlint-disable` like every other rule. |
|
|
12
|
+
| `dynamic-import/no-unexplained-dynamic-import` | `./no-unexplained-dynamic-import` | Static imports are the default because bundlers, knip and typecheck can see them. A dynamic `import()` needs a `// dynamic-import -- reason` comment on the line above its statement, above the import, or on the import itself. |
|
|
13
|
+
| `import-boundaries/no-cross-package-relative-imports` | `./no-cross-package-relative-imports` | A relative import may not leave its package (the nearest directory with a `package.json`). Cross-package code goes through the target's package name and a declared workspace dependency, so typecheck, knip and task-runner caching see the edge. The message names the target package from its `package.json`, so no scope option is needed. |
|
|
14
|
+
| `env-access/no-raw-env-access` | `./no-raw-env-access` | `process.env` and `import.meta.env` are read only through the generated `@<scope>/env` package (Effect `Config`), which validates every variable and keeps secrets out of client bundles. Options: `envPackage` (name shown in the message) and `allowedPackages` (package names whose files may read the raw environment; default: any package named `@<anything>/env`). |
|
|
15
|
+
| `route-active/no-render-time-match-route` | `./no-render-time-match-route` | A render-time call of a `useMatchRoute()` result is memoized forever by the React Compiler (TanStack/router#4499), so active states freeze on mount. Event-time calls are fine. The file documents when the rule can be deleted. |
|
|
16
|
+
| `backdrop-filter/no-backdrop-filter` | `./no-backdrop-filter` | Tailwind `backdrop-*` filter utilities, `[backdrop-filter:…]` and `backdropFilter` style keys re-filter everything behind the element every frame and make open/close animations stutter. |
|
|
17
|
+
|
|
18
|
+
Retired: `unknown-cast/require-comment` (`anti-slop/no-chained-type-assertions` covers `as unknown as`) and `prefer-effect/*` (covered upstream by `@effect/tsgo`).
|
|
19
|
+
|
|
20
|
+
## Vendored anti-slop
|
|
21
|
+
|
|
22
|
+
`./vendor/anti-slop` (plugin `anti-slop`) and `./vendor/anti-slop/effect` (plugin `anti-slop-effect`) are dmmulroy/anti-slop at `c44ef22`, MIT. See [`src/vendor/anti-slop/UPSTREAM.md`](src/vendor/anti-slop/UPSTREAM.md) for the local delta (`?? null`), how to refresh, and why the tree is excluded from lint and format. `shoe-box-oxlint-config` turns every rule on and off-lists the rejected ones with reasons.
|
|
23
|
+
|
|
24
|
+
## Development
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
bun run test # custom rule suites (test/) + vendored RuleTester suites
|
|
28
|
+
bun run typecheck
|
|
29
|
+
bun run build # dist/ for publishing
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Tests use oxlint's own `RuleTester` (`oxlint/plugins-dev`). The path-based rules run against the fixture repo in `test/fixtures/repo`. `@oxlint/plugins` must match the `oxlint` version exactly; both are pinned in the root catalog.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disallows CSS backdrop filters: Tailwind `backdrop-blur-*` and the other
|
|
3
|
+
* `backdrop-*` filter utilities, `[backdrop-filter:...]` arbitrary properties,
|
|
4
|
+
* and `backdropFilter` keys in style or animation objects.
|
|
5
|
+
*
|
|
6
|
+
* A backdrop filter re-samples and re-filters everything behind the element on
|
|
7
|
+
* every frame the element or its backdrop changes. On a menu or dialog that
|
|
8
|
+
* animates open, that turned a 150 ms zoom into two or three visible frames.
|
|
9
|
+
*/
|
|
10
|
+
declare const _default: import("@oxlint/plugins").Plugin;
|
|
11
|
+
export { _default as default };
|
|
12
|
+
//# sourceMappingURL=no-backdrop-filter.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-backdrop-filter.d.mts","names":[],"sources":["../src/no-backdrop-filter.ts"],"mappings":""}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { definePlugin, defineRule } from "@oxlint/plugins";
|
|
2
|
+
// src/no-backdrop-filter.ts
|
|
3
|
+
/**
|
|
4
|
+
* Disallows CSS backdrop filters: Tailwind `backdrop-blur-*` and the other
|
|
5
|
+
* `backdrop-*` filter utilities, `[backdrop-filter:...]` arbitrary properties,
|
|
6
|
+
* and `backdropFilter` keys in style or animation objects.
|
|
7
|
+
*
|
|
8
|
+
* A backdrop filter re-samples and re-filters everything behind the element on
|
|
9
|
+
* every frame the element or its backdrop changes. On a menu or dialog that
|
|
10
|
+
* animates open, that turned a 150 ms zoom into two or three visible frames.
|
|
11
|
+
*/
|
|
12
|
+
const BACKDROP_CLASS_RE = /backdrop-(?:blur|brightness|contrast|grayscale|hue-rotate|invert|opacity|saturate|sepia|filter)\b/;
|
|
13
|
+
const BACKDROP_STYLE_KEYS = /* @__PURE__ */ new Set(["backdropFilter", "WebkitBackdropFilter"]);
|
|
14
|
+
const rule = defineRule({
|
|
15
|
+
meta: {
|
|
16
|
+
type: "problem",
|
|
17
|
+
docs: { description: "Disallow backdrop filters; they re-filter the backdrop every frame and make animations stutter." },
|
|
18
|
+
messages: { noBackdropFilter: "Backdrop filters re-filter everything behind the element on every frame and make open/close animations stutter. Use an opaque or semi-opaque background instead." }
|
|
19
|
+
},
|
|
20
|
+
create(context) {
|
|
21
|
+
return {
|
|
22
|
+
Literal(node) {
|
|
23
|
+
if (BACKDROP_CLASS_RE.test(String(node.value))) context.report({
|
|
24
|
+
node,
|
|
25
|
+
messageId: "noBackdropFilter"
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
TemplateElement(node) {
|
|
29
|
+
if (BACKDROP_CLASS_RE.test(node.value.raw)) context.report({
|
|
30
|
+
node,
|
|
31
|
+
messageId: "noBackdropFilter"
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
Property(node) {
|
|
35
|
+
if (node.key.type === "Identifier" && !node.computed && BACKDROP_STYLE_KEYS.has(node.key.name)) context.report({
|
|
36
|
+
node: node.key,
|
|
37
|
+
messageId: "noBackdropFilter"
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
var no_backdrop_filter_default = definePlugin({
|
|
44
|
+
meta: { name: "backdrop-filter" },
|
|
45
|
+
rules: { "no-backdrop-filter": rule }
|
|
46
|
+
});
|
|
47
|
+
export { no_backdrop_filter_default as default };
|
|
48
|
+
|
|
49
|
+
//# sourceMappingURL=no-backdrop-filter.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-backdrop-filter.mjs","names":[],"sources":["../src/no-backdrop-filter.ts"],"sourcesContent":["/**\n * Disallows CSS backdrop filters: Tailwind `backdrop-blur-*` and the other\n * `backdrop-*` filter utilities, `[backdrop-filter:...]` arbitrary properties,\n * and `backdropFilter` keys in style or animation objects.\n *\n * A backdrop filter re-samples and re-filters everything behind the element on\n * every frame the element or its backdrop changes. On a menu or dialog that\n * animates open, that turned a 150 ms zoom into two or three visible frames.\n */\n\nimport { definePlugin, defineRule } from '@oxlint/plugins';\n\nconst BACKDROP_CLASS_RE =\n /backdrop-(?:blur|brightness|contrast|grayscale|hue-rotate|invert|opacity|saturate|sepia|filter)\\b/;\n\nconst BACKDROP_STYLE_KEYS = new Set(['backdropFilter', 'WebkitBackdropFilter']);\n\nconst rule = defineRule({\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Disallow backdrop filters; they re-filter the backdrop every frame and make animations stutter.',\n },\n messages: {\n noBackdropFilter:\n 'Backdrop filters re-filter everything behind the element on every frame and make open/close ' +\n 'animations stutter. Use an opaque or semi-opaque background instead.',\n },\n },\n create(context) {\n return {\n Literal(node) {\n if (BACKDROP_CLASS_RE.test(String(node.value))) {\n context.report({ node, messageId: 'noBackdropFilter' });\n }\n },\n TemplateElement(node) {\n if (BACKDROP_CLASS_RE.test(node.value.raw)) {\n context.report({ node, messageId: 'noBackdropFilter' });\n }\n },\n Property(node) {\n if (\n node.key.type === 'Identifier' &&\n !node.computed &&\n BACKDROP_STYLE_KEYS.has(node.key.name)\n ) {\n context.report({ node: node.key, messageId: 'noBackdropFilter' });\n }\n },\n };\n },\n});\n\nexport default definePlugin({\n meta: { name: 'backdrop-filter' },\n rules: { 'no-backdrop-filter': rule },\n});\n"],"mappings":";;;;;;;;;;;AAYA,MAAM,oBACJ;AAEF,MAAM,sCAAsB,IAAI,IAAI,CAAC,kBAAkB,sBAAsB,CAAC;AAE9E,MAAM,OAAO,WAAW;CACtB,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,kGACJ;EACA,UAAU,EACR,kBACE,mKAEJ;CACF;CACA,OAAO,SAAS;EACd,OAAO;GACL,QAAQ,MAAM;IACZ,IAAI,kBAAkB,KAAK,OAAO,KAAK,KAAK,CAAC,GAC3C,QAAQ,OAAO;KAAE;KAAM,WAAW;IAAmB,CAAC;GAE1D;GACA,gBAAgB,MAAM;IACpB,IAAI,kBAAkB,KAAK,KAAK,MAAM,GAAG,GACvC,QAAQ,OAAO;KAAE;KAAM,WAAW;IAAmB,CAAC;GAE1D;GACA,SAAS,MAAM;IACb,IACE,KAAK,IAAI,SAAS,gBAClB,CAAC,KAAK,YACN,oBAAoB,IAAI,KAAK,IAAI,IAAI,GAErC,QAAQ,OAAO;KAAE,MAAM,KAAK;KAAK,WAAW;IAAmB,CAAC;GAEpE;EACF;CACF;AACF,CAAC;AAED,IAAA,6BAAe,aAAa;CAC1B,MAAM,EAAE,MAAM,kBAAkB;CAChC,OAAO,EAAE,sBAAsB,KAAK;AACtC,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disallows relative imports that resolve outside the importing file's
|
|
3
|
+
* package (the nearest ancestor directory containing a package.json).
|
|
4
|
+
*
|
|
5
|
+
* Cross-package code must be imported through the target package's public
|
|
6
|
+
* specifier (its package.json `name`) with a declared workspace dependency, so
|
|
7
|
+
* the dependency graph that typecheck, knip, and task-runner caching rely on
|
|
8
|
+
* can see the edge. A relative path that climbs out of the package is
|
|
9
|
+
* invisible to all of them, so a deleted or renamed export can keep compiling
|
|
10
|
+
* unnoticed.
|
|
11
|
+
*/
|
|
12
|
+
declare const _default: import("@oxlint/plugins").Plugin;
|
|
13
|
+
export { _default as default };
|
|
14
|
+
//# sourceMappingURL=no-cross-package-relative-imports.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-cross-package-relative-imports.d.mts","names":[],"sources":["../src/no-cross-package-relative-imports.ts"],"mappings":""}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { n as readPackageName, t as findPackageRoot } from "./package-root-DzPJ3Pq6.mjs";
|
|
2
|
+
import { definePlugin, defineRule } from "@oxlint/plugins";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
// src/no-cross-package-relative-imports.ts
|
|
6
|
+
/**
|
|
7
|
+
* Disallows relative imports that resolve outside the importing file's
|
|
8
|
+
* package (the nearest ancestor directory containing a package.json).
|
|
9
|
+
*
|
|
10
|
+
* Cross-package code must be imported through the target package's public
|
|
11
|
+
* specifier (its package.json `name`) with a declared workspace dependency, so
|
|
12
|
+
* the dependency graph that typecheck, knip, and task-runner caching rely on
|
|
13
|
+
* can see the edge. A relative path that climbs out of the package is
|
|
14
|
+
* invisible to all of them, so a deleted or renamed export can keep compiling
|
|
15
|
+
* unnoticed.
|
|
16
|
+
*/
|
|
17
|
+
const targetDirOf = (resolved) => fs.existsSync(resolved) && fs.statSync(resolved).isDirectory() ? resolved : path.dirname(resolved);
|
|
18
|
+
const isStringLiteral = (node) => node.type === "Literal" && typeof node.value === "string";
|
|
19
|
+
const checkSource = (context, source) => {
|
|
20
|
+
const spec = source.value;
|
|
21
|
+
if (!spec.startsWith("..")) return;
|
|
22
|
+
const filename = context.physicalFilename;
|
|
23
|
+
if (!path.isAbsolute(filename)) return;
|
|
24
|
+
const fileDir = path.dirname(filename);
|
|
25
|
+
const fileRoot = findPackageRoot(fileDir);
|
|
26
|
+
if (fileRoot === null) return;
|
|
27
|
+
const cleanSpec = spec.split("?")[0] ?? spec;
|
|
28
|
+
const targetRoot = findPackageRoot(targetDirOf(path.resolve(fileDir, cleanSpec)));
|
|
29
|
+
if (targetRoot === fileRoot) return;
|
|
30
|
+
const targetName = targetRoot === null ? null : readPackageName(targetRoot);
|
|
31
|
+
context.report({
|
|
32
|
+
node: source,
|
|
33
|
+
messageId: "noCrossPackageRelativeImport",
|
|
34
|
+
data: {
|
|
35
|
+
spec,
|
|
36
|
+
targetPackage: targetName ?? (targetRoot === null ? "(no package found)" : path.relative(context.cwd, targetRoot) || ".")
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
const rule = defineRule({
|
|
41
|
+
meta: {
|
|
42
|
+
type: "problem",
|
|
43
|
+
docs: { description: "Disallow relative imports that resolve outside the importing package" },
|
|
44
|
+
messages: { noCrossPackageRelativeImport: "Relative import '{{spec}}' resolves outside this package (into {{targetPackage}}). Import through the target package’s public specifier and declare a workspace dependency instead — relative cross-package edges are invisible to typecheck, knip, and task-runner caching." }
|
|
45
|
+
},
|
|
46
|
+
create(context) {
|
|
47
|
+
return {
|
|
48
|
+
ImportDeclaration(node) {
|
|
49
|
+
checkSource(context, node.source);
|
|
50
|
+
},
|
|
51
|
+
ImportExpression(node) {
|
|
52
|
+
if (isStringLiteral(node.source)) checkSource(context, node.source);
|
|
53
|
+
},
|
|
54
|
+
ExportNamedDeclaration(node) {
|
|
55
|
+
if (node.source !== null) checkSource(context, node.source);
|
|
56
|
+
},
|
|
57
|
+
ExportAllDeclaration(node) {
|
|
58
|
+
checkSource(context, node.source);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
var no_cross_package_relative_imports_default = definePlugin({
|
|
64
|
+
meta: { name: "import-boundaries" },
|
|
65
|
+
rules: { "no-cross-package-relative-imports": rule }
|
|
66
|
+
});
|
|
67
|
+
export { no_cross_package_relative_imports_default as default };
|
|
68
|
+
|
|
69
|
+
//# sourceMappingURL=no-cross-package-relative-imports.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-cross-package-relative-imports.mjs","names":[],"sources":["../src/no-cross-package-relative-imports.ts"],"sourcesContent":["/**\n * Disallows relative imports that resolve outside the importing file's\n * package (the nearest ancestor directory containing a package.json).\n *\n * Cross-package code must be imported through the target package's public\n * specifier (its package.json `name`) with a declared workspace dependency, so\n * the dependency graph that typecheck, knip, and task-runner caching rely on\n * can see the edge. A relative path that climbs out of the package is\n * invisible to all of them, so a deleted or renamed export can keep compiling\n * unnoticed.\n */\n\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nimport { definePlugin, defineRule } from '@oxlint/plugins';\nimport type { Context, ESTree } from '@oxlint/plugins';\n\nimport { findPackageRoot, readPackageName } from './internal/package-root.ts';\n\nconst targetDirOf = (resolved: string) =>\n fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()\n ? resolved\n : path.dirname(resolved);\n\nconst isStringLiteral = (\n node: ESTree.Expression,\n): node is ESTree.StringLiteral =>\n node.type === 'Literal' && typeof node.value === 'string';\n\nconst checkSource = (context: Context, source: ESTree.StringLiteral) => {\n const spec = source.value;\n // Only parent-relative specifiers can escape the package.\n if (!spec.startsWith('..')) return;\n\n const filename = context.physicalFilename;\n if (!path.isAbsolute(filename)) return;\n const fileDir = path.dirname(filename);\n const fileRoot = findPackageRoot(fileDir);\n if (fileRoot === null) return;\n\n // Strip bundler query suffixes (`?raw`, `?react`, ...).\n const cleanSpec = spec.split('?')[0] ?? spec;\n const targetRoot = findPackageRoot(\n targetDirOf(path.resolve(fileDir, cleanSpec)),\n );\n if (targetRoot === fileRoot) return;\n\n const targetName = targetRoot === null ? null : readPackageName(targetRoot);\n context.report({\n node: source,\n messageId: 'noCrossPackageRelativeImport',\n data: {\n spec,\n targetPackage:\n targetName ??\n (targetRoot === null\n ? '(no package found)'\n : path.relative(context.cwd, targetRoot) || '.'),\n },\n });\n};\n\nconst rule = defineRule({\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Disallow relative imports that resolve outside the importing package',\n },\n messages: {\n noCrossPackageRelativeImport:\n \"Relative import '{{spec}}' resolves outside this package (into {{targetPackage}}). \" +\n 'Import through the target package’s public specifier and declare a workspace ' +\n 'dependency instead — relative cross-package edges are invisible to typecheck, ' +\n 'knip, and task-runner caching.',\n },\n },\n create(context) {\n return {\n ImportDeclaration(node) {\n checkSource(context, node.source);\n },\n ImportExpression(node) {\n if (isStringLiteral(node.source)) checkSource(context, node.source);\n },\n ExportNamedDeclaration(node) {\n if (node.source !== null) checkSource(context, node.source);\n },\n ExportAllDeclaration(node) {\n checkSource(context, node.source);\n },\n };\n },\n});\n\nexport default definePlugin({\n meta: { name: 'import-boundaries' },\n rules: { 'no-cross-package-relative-imports': rule },\n});\n"],"mappings":";;;;;;;;;;;;;;;;AAoBA,MAAM,eAAe,aACnB,GAAG,WAAW,QAAQ,KAAK,GAAG,SAAS,QAAQ,CAAC,CAAC,YAAY,IACzD,WACA,KAAK,QAAQ,QAAQ;AAE3B,MAAM,mBACJ,SAEA,KAAK,SAAS,aAAa,OAAO,KAAK,UAAU;AAEnD,MAAM,eAAe,SAAkB,WAAiC;CACtE,MAAM,OAAO,OAAO;CAEpB,IAAI,CAAC,KAAK,WAAW,IAAI,GAAG;CAE5B,MAAM,WAAW,QAAQ;CACzB,IAAI,CAAC,KAAK,WAAW,QAAQ,GAAG;CAChC,MAAM,UAAU,KAAK,QAAQ,QAAQ;CACrC,MAAM,WAAW,gBAAgB,OAAO;CACxC,IAAI,aAAa,MAAM;CAGvB,MAAM,YAAY,KAAK,MAAM,GAAG,CAAC,CAAC,MAAM;CACxC,MAAM,aAAa,gBACjB,YAAY,KAAK,QAAQ,SAAS,SAAS,CAAC,CAC9C;CACA,IAAI,eAAe,UAAU;CAE7B,MAAM,aAAa,eAAe,OAAO,OAAO,gBAAgB,UAAU;CAC1E,QAAQ,OAAO;EACb,MAAM;EACN,WAAW;EACX,MAAM;GACJ;GACA,eACE,eACC,eAAe,OACZ,uBACA,KAAK,SAAS,QAAQ,KAAK,UAAU,KAAK;EAClD;CACF,CAAC;AACH;AAEA,MAAM,OAAO,WAAW;CACtB,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,uEACJ;EACA,UAAU,EACR,8BACE,+QAIJ;CACF;CACA,OAAO,SAAS;EACd,OAAO;GACL,kBAAkB,MAAM;IACtB,YAAY,SAAS,KAAK,MAAM;GAClC;GACA,iBAAiB,MAAM;IACrB,IAAI,gBAAgB,KAAK,MAAM,GAAG,YAAY,SAAS,KAAK,MAAM;GACpE;GACA,uBAAuB,MAAM;IAC3B,IAAI,KAAK,WAAW,MAAM,YAAY,SAAS,KAAK,MAAM;GAC5D;GACA,qBAAqB,MAAM;IACzB,YAAY,SAAS,KAAK,MAAM;GAClC;EACF;CACF;AACF,CAAC;AAED,IAAA,4CAAe,aAAa;CAC1B,MAAM,EAAE,MAAM,oBAAoB;CAClC,OAAO,EAAE,qCAAqC,KAAK;AACrD,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disallows direct access to `import.meta.env` or `process.env`.
|
|
3
|
+
*
|
|
4
|
+
* App code reads environment variables through Effect `Config`, which
|
|
5
|
+
* validates them and keeps configuration swappable in tests, or through a
|
|
6
|
+
* repo's own env package where it has one. Files inside such a package are
|
|
7
|
+
* exempt: reading the raw environment is its job.
|
|
8
|
+
*
|
|
9
|
+
* Options (all optional):
|
|
10
|
+
* - `envPackage`: an env package to name in the message instead of Effect
|
|
11
|
+
* `Config`.
|
|
12
|
+
* - `allowedPackages`: package.json names whose files may read the raw
|
|
13
|
+
* environment (default: any package named `@<anything>/env`).
|
|
14
|
+
*/
|
|
15
|
+
declare const _default: import("@oxlint/plugins").Plugin;
|
|
16
|
+
export { _default as default };
|
|
17
|
+
//# sourceMappingURL=no-raw-env-access.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-raw-env-access.d.mts","names":[],"sources":["../src/no-raw-env-access.ts"],"mappings":""}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { n as readPackageName, t as findPackageRoot } from "./package-root-DzPJ3Pq6.mjs";
|
|
2
|
+
import { definePlugin, defineRule } from "@oxlint/plugins";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
// src/no-raw-env-access.ts
|
|
5
|
+
/**
|
|
6
|
+
* Disallows direct access to `import.meta.env` or `process.env`.
|
|
7
|
+
*
|
|
8
|
+
* App code reads environment variables through Effect `Config`, which
|
|
9
|
+
* validates them and keeps configuration swappable in tests, or through a
|
|
10
|
+
* repo's own env package where it has one. Files inside such a package are
|
|
11
|
+
* exempt: reading the raw environment is its job.
|
|
12
|
+
*
|
|
13
|
+
* Options (all optional):
|
|
14
|
+
* - `envPackage`: an env package to name in the message instead of Effect
|
|
15
|
+
* `Config`.
|
|
16
|
+
* - `allowedPackages`: package.json names whose files may read the raw
|
|
17
|
+
* environment (default: any package named `@<anything>/env`).
|
|
18
|
+
*/
|
|
19
|
+
const defaultEnvPackagePattern = /^@[^/]+\/env$/u;
|
|
20
|
+
const readOptions = (context) => {
|
|
21
|
+
const [first] = context.options;
|
|
22
|
+
const { envPackage, allowedPackages } = first !== null && typeof first === "object" && !Array.isArray(first) ? first : {};
|
|
23
|
+
return {
|
|
24
|
+
envPackage: typeof envPackage === "string" ? envPackage : null,
|
|
25
|
+
allowedPackages: Array.isArray(allowedPackages) ? allowedPackages.filter((name) => typeof name === "string") : null
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
const isInAllowedPackage = (context, options) => {
|
|
29
|
+
const filename = context.physicalFilename;
|
|
30
|
+
if (!path.isAbsolute(filename)) return false;
|
|
31
|
+
const root = findPackageRoot(path.dirname(filename));
|
|
32
|
+
const name = root === null ? null : readPackageName(root);
|
|
33
|
+
if (name === null) return false;
|
|
34
|
+
return options.allowedPackages === null ? defaultEnvPackagePattern.test(name) : options.allowedPackages.includes(name);
|
|
35
|
+
};
|
|
36
|
+
const propertyNamed = (node, name) => node.computed ? node.property.type === "Literal" && node.property.value === name : node.property.type === "Identifier" && node.property.name === name;
|
|
37
|
+
const isProcessEnv = (node) => node.object.type === "Identifier" && node.object.name === "process" && propertyNamed(node, "env");
|
|
38
|
+
const isImportMetaEnv = (node) => node.object.type === "MetaProperty" && node.object.meta.name === "import" && node.object.property.name === "meta" && propertyNamed(node, "env");
|
|
39
|
+
const rule = defineRule({
|
|
40
|
+
meta: {
|
|
41
|
+
type: "problem",
|
|
42
|
+
docs: { description: "Disallow direct access to import.meta.env or process.env; read env through Effect Config." },
|
|
43
|
+
schema: [{
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
envPackage: { type: "string" },
|
|
47
|
+
allowedPackages: {
|
|
48
|
+
type: "array",
|
|
49
|
+
items: { type: "string" }
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
additionalProperties: false
|
|
53
|
+
}],
|
|
54
|
+
messages: {
|
|
55
|
+
noImportMetaEnv: "Direct access to `import.meta.env` is not allowed. Read it through {{via}} instead: it validates every variable and keeps secrets out of client bundles.",
|
|
56
|
+
noProcessEnv: "Direct access to `process.env` is not allowed. Read it through {{via}} instead: it validates every variable and keeps configuration in one place."
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
create(context) {
|
|
60
|
+
const options = readOptions(context);
|
|
61
|
+
if (isInAllowedPackage(context, options)) return {};
|
|
62
|
+
const data = { via: options.envPackage === null ? "Effect `Config`" : `\`${options.envPackage}\`` };
|
|
63
|
+
return { MemberExpression(node) {
|
|
64
|
+
if (isProcessEnv(node)) context.report({
|
|
65
|
+
node,
|
|
66
|
+
messageId: "noProcessEnv",
|
|
67
|
+
data
|
|
68
|
+
});
|
|
69
|
+
else if (isImportMetaEnv(node)) context.report({
|
|
70
|
+
node,
|
|
71
|
+
messageId: "noImportMetaEnv",
|
|
72
|
+
data
|
|
73
|
+
});
|
|
74
|
+
} };
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
var no_raw_env_access_default = definePlugin({
|
|
78
|
+
meta: { name: "env-access" },
|
|
79
|
+
rules: { "no-raw-env-access": rule }
|
|
80
|
+
});
|
|
81
|
+
export { no_raw_env_access_default as default };
|
|
82
|
+
|
|
83
|
+
//# sourceMappingURL=no-raw-env-access.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-raw-env-access.mjs","names":[],"sources":["../src/no-raw-env-access.ts"],"sourcesContent":["/**\n * Disallows direct access to `import.meta.env` or `process.env`.\n *\n * App code reads environment variables through Effect `Config`, which\n * validates them and keeps configuration swappable in tests, or through a\n * repo's own env package where it has one. Files inside such a package are\n * exempt: reading the raw environment is its job.\n *\n * Options (all optional):\n * - `envPackage`: an env package to name in the message instead of Effect\n * `Config`.\n * - `allowedPackages`: package.json names whose files may read the raw\n * environment (default: any package named `@<anything>/env`).\n */\n\nimport path from 'node:path';\n\nimport { definePlugin, defineRule } from '@oxlint/plugins';\nimport type { Context, ESTree } from '@oxlint/plugins';\n\nimport { findPackageRoot, readPackageName } from './internal/package-root.ts';\n\nconst defaultEnvPackagePattern = /^@[^/]+\\/env$/u;\n\ntype EnvAccessOptions = {\n envPackage: string | null;\n allowedPackages: ReadonlyArray<string> | null;\n};\n\nconst readOptions = (context: Context): EnvAccessOptions => {\n const [first] = context.options;\n const options =\n first !== null && typeof first === 'object' && !Array.isArray(first)\n ? first\n : {};\n const { envPackage, allowedPackages } = options;\n return {\n envPackage: typeof envPackage === 'string' ? envPackage : null,\n allowedPackages: Array.isArray(allowedPackages)\n ? allowedPackages.filter((name) => typeof name === 'string')\n : null,\n };\n};\n\nconst isInAllowedPackage = (context: Context, options: EnvAccessOptions) => {\n const filename = context.physicalFilename;\n if (!path.isAbsolute(filename)) return false;\n const root = findPackageRoot(path.dirname(filename));\n const name = root === null ? null : readPackageName(root);\n if (name === null) return false;\n return options.allowedPackages === null\n ? defaultEnvPackagePattern.test(name)\n : options.allowedPackages.includes(name);\n};\n\nconst propertyNamed = (node: ESTree.MemberExpression, name: string) =>\n node.computed\n ? node.property.type === 'Literal' && node.property.value === name\n : node.property.type === 'Identifier' && node.property.name === name;\n\nconst isProcessEnv = (node: ESTree.MemberExpression) =>\n node.object.type === 'Identifier' &&\n node.object.name === 'process' &&\n propertyNamed(node, 'env');\n\nconst isImportMetaEnv = (node: ESTree.MemberExpression) =>\n node.object.type === 'MetaProperty' &&\n node.object.meta.name === 'import' &&\n node.object.property.name === 'meta' &&\n propertyNamed(node, 'env');\n\nconst rule = defineRule({\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Disallow direct access to import.meta.env or process.env; read env through Effect Config.',\n },\n schema: [\n {\n type: 'object',\n properties: {\n envPackage: { type: 'string' },\n allowedPackages: { type: 'array', items: { type: 'string' } },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n noImportMetaEnv:\n 'Direct access to `import.meta.env` is not allowed. Read it through {{via}} instead: ' +\n 'it validates every variable and keeps secrets out of client bundles.',\n noProcessEnv:\n 'Direct access to `process.env` is not allowed. Read it through {{via}} instead: ' +\n 'it validates every variable and keeps configuration in one place.',\n },\n },\n create(context) {\n const options = readOptions(context);\n if (isInAllowedPackage(context, options)) return {};\n const data = {\n via:\n options.envPackage === null\n ? 'Effect `Config`'\n : `\\`${options.envPackage}\\``,\n };\n\n return {\n // `process.env.X` and `import.meta.env.X` contain the bare form, so\n // matching only the bare member reports each access exactly once.\n MemberExpression(node) {\n if (isProcessEnv(node)) {\n context.report({ node, messageId: 'noProcessEnv', data });\n } else if (isImportMetaEnv(node)) {\n context.report({ node, messageId: 'noImportMetaEnv', data });\n }\n },\n };\n },\n});\n\nexport default definePlugin({\n meta: { name: 'env-access' },\n rules: { 'no-raw-env-access': rule },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;AAsBA,MAAM,2BAA2B;AAOjC,MAAM,eAAe,YAAuC;CAC1D,MAAM,CAAC,SAAS,QAAQ;CAKxB,MAAM,EAAE,YAAY,oBAHlB,UAAU,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,IAC/D,QACA,CAAC;CAEP,OAAO;EACL,YAAY,OAAO,eAAe,WAAW,aAAa;EAC1D,iBAAiB,MAAM,QAAQ,eAAe,IAC1C,gBAAgB,QAAQ,SAAS,OAAO,SAAS,QAAQ,IACzD;CACN;AACF;AAEA,MAAM,sBAAsB,SAAkB,YAA8B;CAC1E,MAAM,WAAW,QAAQ;CACzB,IAAI,CAAC,KAAK,WAAW,QAAQ,GAAG,OAAO;CACvC,MAAM,OAAO,gBAAgB,KAAK,QAAQ,QAAQ,CAAC;CACnD,MAAM,OAAO,SAAS,OAAO,OAAO,gBAAgB,IAAI;CACxD,IAAI,SAAS,MAAM,OAAO;CAC1B,OAAO,QAAQ,oBAAoB,OAC/B,yBAAyB,KAAK,IAAI,IAClC,QAAQ,gBAAgB,SAAS,IAAI;AAC3C;AAEA,MAAM,iBAAiB,MAA+B,SACpD,KAAK,WACD,KAAK,SAAS,SAAS,aAAa,KAAK,SAAS,UAAU,OAC5D,KAAK,SAAS,SAAS,gBAAgB,KAAK,SAAS,SAAS;AAEpE,MAAM,gBAAgB,SACpB,KAAK,OAAO,SAAS,gBACrB,KAAK,OAAO,SAAS,aACrB,cAAc,MAAM,KAAK;AAE3B,MAAM,mBAAmB,SACvB,KAAK,OAAO,SAAS,kBACrB,KAAK,OAAO,KAAK,SAAS,YAC1B,KAAK,OAAO,SAAS,SAAS,UAC9B,cAAc,MAAM,KAAK;AAE3B,MAAM,OAAO,WAAW;CACtB,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,4FACJ;EACA,QAAQ,CACN;GACE,MAAM;GACN,YAAY;IACV,YAAY,EAAE,MAAM,SAAS;IAC7B,iBAAiB;KAAE,MAAM;KAAS,OAAO,EAAE,MAAM,SAAS;IAAE;GAC9D;GACA,sBAAsB;EACxB,CACF;EACA,UAAU;GACR,iBACE;GAEF,cACE;EAEJ;CACF;CACA,OAAO,SAAS;EACd,MAAM,UAAU,YAAY,OAAO;EACnC,IAAI,mBAAmB,SAAS,OAAO,GAAG,OAAO,CAAC;EAClD,MAAM,OAAO,EACX,KACE,QAAQ,eAAe,OACnB,oBACA,KAAK,QAAQ,WAAW,IAChC;EAEA,OAAO,EAGL,iBAAiB,MAAM;GACrB,IAAI,aAAa,IAAI,GACnB,QAAQ,OAAO;IAAE;IAAM,WAAW;IAAgB;GAAK,CAAC;QACnD,IAAI,gBAAgB,IAAI,GAC7B,QAAQ,OAAO;IAAE;IAAM,WAAW;IAAmB;GAAK,CAAC;EAE/D,EACF;CACF;AACF,CAAC;AAED,IAAA,4BAAe,aAAa;CAC1B,MAAM,EAAE,MAAM,aAAa;CAC3B,OAAO,EAAE,qBAAqB,KAAK;AACrC,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disallows invoking a `useMatchRoute()` result during render.
|
|
3
|
+
*
|
|
4
|
+
* `useMatchRoute` returns a referentially stable callback that reads mutable
|
|
5
|
+
* router state when invoked. A render-time `matchRoute({ ... })` call is
|
|
6
|
+
* therefore an ordinary expression whose only inputs — the callback and the
|
|
7
|
+
* options object — never change identity, so `babel-plugin-react-compiler`
|
|
8
|
+
* stores the result in a memo cell on first render and serves that value
|
|
9
|
+
* forever. Active states computed this way freeze on mount and never follow
|
|
10
|
+
* navigation.
|
|
11
|
+
*
|
|
12
|
+
* Calls from a nested callback (event handlers, mutation lifecycles, store
|
|
13
|
+
* selectors) are fine: they run after render against live router state, so this
|
|
14
|
+
* rule judges the call site rather than the import. No file-level exemptions.
|
|
15
|
+
*
|
|
16
|
+
* The fix is to evaluate the match inside a `useRouterState` selector
|
|
17
|
+
* (`useRouterState({ select: (s) => matches(s.location) })`). The compiler
|
|
18
|
+
* never memoizes a hook call, so its return value is recomputed every render.
|
|
19
|
+
*
|
|
20
|
+
* `'use no memo'` is not an alternative: the directive applies only to the
|
|
21
|
+
* function it is written in, so it would have to be repeated on every consuming
|
|
22
|
+
* component and would disable memoization for all of each one.
|
|
23
|
+
*
|
|
24
|
+
* ---------------------------------------------------------------------------
|
|
25
|
+
* WHEN TO DELETE THIS RULE
|
|
26
|
+
* ---------------------------------------------------------------------------
|
|
27
|
+
* This is a workaround for an upstream bug. Delete this rule, its `jsPlugins`
|
|
28
|
+
* entry, and its `overrides` block in oxlint.config.ts once EITHER of these
|
|
29
|
+
* lands in our pinned `@tanstack/react-router`:
|
|
30
|
+
*
|
|
31
|
+
* 1. `useMatchRoute`'s returned callback changes identity when router state
|
|
32
|
+
* changes. Tracking issue: https://github.com/TanStack/router/issues/4499
|
|
33
|
+
* Proposed fix: https://github.com/TanStack/router/pull/6561
|
|
34
|
+
* As of react-router@1.170.18 the fix is still unmerged — a maintainer
|
|
35
|
+
* asked for it to be reworked against https://github.com/TanStack/router/pull/7805,
|
|
36
|
+
* which added a `matchRouteDeps` store subscription. That subscription
|
|
37
|
+
* makes the component re-render, but leaves the callback identity stable
|
|
38
|
+
* (`useCallback(..., [router])`), so it does NOT fix this.
|
|
39
|
+
*
|
|
40
|
+
* 2. The React Compiler learns that `@tanstack/react-router` is incompatible.
|
|
41
|
+
* It already auto-bails on `react-hook-form`'s `useForm().watch`,
|
|
42
|
+
* `@tanstack/react-table` and `@tanstack/react-virtual` via
|
|
43
|
+
* `defaultModuleTypeProvider`; the router is simply absent from that list.
|
|
44
|
+
* If it is added, affected components bail out on their own.
|
|
45
|
+
*
|
|
46
|
+
* Caveat for the eventual move to the Rust/oxc React Compiler
|
|
47
|
+
* (https://github.com/vitejs/vite/discussions/22949): switching compilers does
|
|
48
|
+
* not by itself make render-time `matchRoute()` safe. It reimplements the same
|
|
49
|
+
* memoization, so the same freeze applies unless it also ships the
|
|
50
|
+
* module-type-provider bailout above. Re-verify rather than assume — compile a
|
|
51
|
+
* consumer and check whether the `matchRoute(...)` call sits inside a memo gate
|
|
52
|
+
* keyed only on stable values.
|
|
53
|
+
*/
|
|
54
|
+
declare const _default: import("@oxlint/plugins").Plugin;
|
|
55
|
+
export { _default as default };
|
|
56
|
+
//# sourceMappingURL=no-render-time-match-route.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-render-time-match-route.d.mts","names":[],"sources":["../src/no-render-time-match-route.ts"],"mappings":""}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { definePlugin, defineRule } from "@oxlint/plugins";
|
|
2
|
+
// src/no-render-time-match-route.ts
|
|
3
|
+
/**
|
|
4
|
+
* Disallows invoking a `useMatchRoute()` result during render.
|
|
5
|
+
*
|
|
6
|
+
* `useMatchRoute` returns a referentially stable callback that reads mutable
|
|
7
|
+
* router state when invoked. A render-time `matchRoute({ ... })` call is
|
|
8
|
+
* therefore an ordinary expression whose only inputs — the callback and the
|
|
9
|
+
* options object — never change identity, so `babel-plugin-react-compiler`
|
|
10
|
+
* stores the result in a memo cell on first render and serves that value
|
|
11
|
+
* forever. Active states computed this way freeze on mount and never follow
|
|
12
|
+
* navigation.
|
|
13
|
+
*
|
|
14
|
+
* Calls from a nested callback (event handlers, mutation lifecycles, store
|
|
15
|
+
* selectors) are fine: they run after render against live router state, so this
|
|
16
|
+
* rule judges the call site rather than the import. No file-level exemptions.
|
|
17
|
+
*
|
|
18
|
+
* The fix is to evaluate the match inside a `useRouterState` selector
|
|
19
|
+
* (`useRouterState({ select: (s) => matches(s.location) })`). The compiler
|
|
20
|
+
* never memoizes a hook call, so its return value is recomputed every render.
|
|
21
|
+
*
|
|
22
|
+
* `'use no memo'` is not an alternative: the directive applies only to the
|
|
23
|
+
* function it is written in, so it would have to be repeated on every consuming
|
|
24
|
+
* component and would disable memoization for all of each one.
|
|
25
|
+
*
|
|
26
|
+
* ---------------------------------------------------------------------------
|
|
27
|
+
* WHEN TO DELETE THIS RULE
|
|
28
|
+
* ---------------------------------------------------------------------------
|
|
29
|
+
* This is a workaround for an upstream bug. Delete this rule, its `jsPlugins`
|
|
30
|
+
* entry, and its `overrides` block in oxlint.config.ts once EITHER of these
|
|
31
|
+
* lands in our pinned `@tanstack/react-router`:
|
|
32
|
+
*
|
|
33
|
+
* 1. `useMatchRoute`'s returned callback changes identity when router state
|
|
34
|
+
* changes. Tracking issue: https://github.com/TanStack/router/issues/4499
|
|
35
|
+
* Proposed fix: https://github.com/TanStack/router/pull/6561
|
|
36
|
+
* As of react-router@1.170.18 the fix is still unmerged — a maintainer
|
|
37
|
+
* asked for it to be reworked against https://github.com/TanStack/router/pull/7805,
|
|
38
|
+
* which added a `matchRouteDeps` store subscription. That subscription
|
|
39
|
+
* makes the component re-render, but leaves the callback identity stable
|
|
40
|
+
* (`useCallback(..., [router])`), so it does NOT fix this.
|
|
41
|
+
*
|
|
42
|
+
* 2. The React Compiler learns that `@tanstack/react-router` is incompatible.
|
|
43
|
+
* It already auto-bails on `react-hook-form`'s `useForm().watch`,
|
|
44
|
+
* `@tanstack/react-table` and `@tanstack/react-virtual` via
|
|
45
|
+
* `defaultModuleTypeProvider`; the router is simply absent from that list.
|
|
46
|
+
* If it is added, affected components bail out on their own.
|
|
47
|
+
*
|
|
48
|
+
* Caveat for the eventual move to the Rust/oxc React Compiler
|
|
49
|
+
* (https://github.com/vitejs/vite/discussions/22949): switching compilers does
|
|
50
|
+
* not by itself make render-time `matchRoute()` safe. It reimplements the same
|
|
51
|
+
* memoization, so the same freeze applies unless it also ships the
|
|
52
|
+
* module-type-provider bailout above. Re-verify rather than assume — compile a
|
|
53
|
+
* consumer and check whether the `matchRoute(...)` call sits inside a memo gate
|
|
54
|
+
* keyed only on stable values.
|
|
55
|
+
*/
|
|
56
|
+
const ROUTER_MODULE = "@tanstack/react-router";
|
|
57
|
+
const MATCH_ROUTE_HOOK = "useMatchRoute";
|
|
58
|
+
/**
|
|
59
|
+
* Hooks whose function argument still runs during render, so a `matchRoute()`
|
|
60
|
+
* call inside one is memoized exactly like a bare render-time call.
|
|
61
|
+
* `useCallback`/`useEffect` are absent on purpose — those defer past render.
|
|
62
|
+
*/
|
|
63
|
+
const RENDER_TIME_CALLBACK_HOOKS = /* @__PURE__ */ new Set(["useMemo"]);
|
|
64
|
+
function isMatchRouteHookCall(node, hookNames) {
|
|
65
|
+
return node?.type === "CallExpression" && node.callee.type === "Identifier" && hookNames.has(node.callee.name);
|
|
66
|
+
}
|
|
67
|
+
/** Resolves the called name for both `useMemo(...)` and `React.useMemo(...)`. */
|
|
68
|
+
function calleeName(callee) {
|
|
69
|
+
if (callee.type === "Identifier") return callee.name;
|
|
70
|
+
if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier") return callee.property.name;
|
|
71
|
+
}
|
|
72
|
+
const rule = defineRule({
|
|
73
|
+
meta: {
|
|
74
|
+
type: "problem",
|
|
75
|
+
docs: { description: "Disallow calling a useMatchRoute() result during render; the React Compiler caches the result forever." },
|
|
76
|
+
messages: { renderTimeMatchRoute: "Calling `{{name}}()` during render freezes the result — the React Compiler memoizes it on the stable callback identity, so it never updates after navigation (TanStack/router#4499). Evaluate the match inside a `useRouterState` selector, or move the call into an event handler if you only need the match at event time." }
|
|
77
|
+
},
|
|
78
|
+
create(context) {
|
|
79
|
+
/** Local names the router's `useMatchRoute` is imported under. */
|
|
80
|
+
const hookNames = /* @__PURE__ */ new Set();
|
|
81
|
+
/** Function arguments of `useMemo`-style hooks, which still run at render. */
|
|
82
|
+
const renderTimeCallbacks = /* @__PURE__ */ new WeakSet();
|
|
83
|
+
const stack = [{
|
|
84
|
+
bindings: /* @__PURE__ */ new Set(),
|
|
85
|
+
runsDuringRender: false
|
|
86
|
+
}];
|
|
87
|
+
function enterFunction(node) {
|
|
88
|
+
stack.push({
|
|
89
|
+
bindings: /* @__PURE__ */ new Set(),
|
|
90
|
+
runsDuringRender: renderTimeCallbacks.has(node)
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function exitFunction() {
|
|
94
|
+
stack.pop();
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
ImportDeclaration(node) {
|
|
98
|
+
if (node.source.value !== ROUTER_MODULE) return;
|
|
99
|
+
for (const specifier of node.specifiers) if (specifier.type === "ImportSpecifier" && specifier.imported.type === "Identifier" && specifier.imported.name === MATCH_ROUTE_HOOK) hookNames.add(specifier.local.name);
|
|
100
|
+
},
|
|
101
|
+
VariableDeclarator(node) {
|
|
102
|
+
if (node.id.type !== "Identifier") return;
|
|
103
|
+
if (!isMatchRouteHookCall(node.init, hookNames)) return;
|
|
104
|
+
stack.at(-1)?.bindings.add(node.id.name);
|
|
105
|
+
},
|
|
106
|
+
CallExpression(node) {
|
|
107
|
+
const called = calleeName(node.callee);
|
|
108
|
+
if (called !== void 0 && RENDER_TIME_CALLBACK_HOOKS.has(called)) {
|
|
109
|
+
const [callback] = node.arguments;
|
|
110
|
+
if (callback?.type === "ArrowFunctionExpression" || callback?.type === "FunctionExpression") renderTimeCallbacks.add(callback);
|
|
111
|
+
}
|
|
112
|
+
if (isMatchRouteHookCall(node.callee, hookNames)) {
|
|
113
|
+
context.report({
|
|
114
|
+
node,
|
|
115
|
+
messageId: "renderTimeMatchRoute",
|
|
116
|
+
data: { name: MATCH_ROUTE_HOOK }
|
|
117
|
+
});
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (node.callee.type !== "Identifier") return;
|
|
121
|
+
const name = node.callee.name;
|
|
122
|
+
const declIndex = stack.findLastIndex((frame) => frame.bindings.has(name));
|
|
123
|
+
if (declIndex === -1) return;
|
|
124
|
+
if (!stack.slice(declIndex + 1).every((frame) => frame.runsDuringRender)) return;
|
|
125
|
+
context.report({
|
|
126
|
+
node,
|
|
127
|
+
messageId: "renderTimeMatchRoute",
|
|
128
|
+
data: { name }
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
FunctionDeclaration: enterFunction,
|
|
132
|
+
"FunctionDeclaration:exit": exitFunction,
|
|
133
|
+
FunctionExpression: enterFunction,
|
|
134
|
+
"FunctionExpression:exit": exitFunction,
|
|
135
|
+
ArrowFunctionExpression: enterFunction,
|
|
136
|
+
"ArrowFunctionExpression:exit": exitFunction
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
var no_render_time_match_route_default = definePlugin({
|
|
141
|
+
meta: { name: "route-active" },
|
|
142
|
+
rules: { "no-render-time-match-route": rule }
|
|
143
|
+
});
|
|
144
|
+
export { no_render_time_match_route_default as default };
|
|
145
|
+
|
|
146
|
+
//# sourceMappingURL=no-render-time-match-route.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-render-time-match-route.mjs","names":[],"sources":["../src/no-render-time-match-route.ts"],"sourcesContent":["/**\n * Disallows invoking a `useMatchRoute()` result during render.\n *\n * `useMatchRoute` returns a referentially stable callback that reads mutable\n * router state when invoked. A render-time `matchRoute({ ... })` call is\n * therefore an ordinary expression whose only inputs — the callback and the\n * options object — never change identity, so `babel-plugin-react-compiler`\n * stores the result in a memo cell on first render and serves that value\n * forever. Active states computed this way freeze on mount and never follow\n * navigation.\n *\n * Calls from a nested callback (event handlers, mutation lifecycles, store\n * selectors) are fine: they run after render against live router state, so this\n * rule judges the call site rather than the import. No file-level exemptions.\n *\n * The fix is to evaluate the match inside a `useRouterState` selector\n * (`useRouterState({ select: (s) => matches(s.location) })`). The compiler\n * never memoizes a hook call, so its return value is recomputed every render.\n *\n * `'use no memo'` is not an alternative: the directive applies only to the\n * function it is written in, so it would have to be repeated on every consuming\n * component and would disable memoization for all of each one.\n *\n * ---------------------------------------------------------------------------\n * WHEN TO DELETE THIS RULE\n * ---------------------------------------------------------------------------\n * This is a workaround for an upstream bug. Delete this rule, its `jsPlugins`\n * entry, and its `overrides` block in oxlint.config.ts once EITHER of these\n * lands in our pinned `@tanstack/react-router`:\n *\n * 1. `useMatchRoute`'s returned callback changes identity when router state\n * changes. Tracking issue: https://github.com/TanStack/router/issues/4499\n * Proposed fix: https://github.com/TanStack/router/pull/6561\n * As of react-router@1.170.18 the fix is still unmerged — a maintainer\n * asked for it to be reworked against https://github.com/TanStack/router/pull/7805,\n * which added a `matchRouteDeps` store subscription. That subscription\n * makes the component re-render, but leaves the callback identity stable\n * (`useCallback(..., [router])`), so it does NOT fix this.\n *\n * 2. The React Compiler learns that `@tanstack/react-router` is incompatible.\n * It already auto-bails on `react-hook-form`'s `useForm().watch`,\n * `@tanstack/react-table` and `@tanstack/react-virtual` via\n * `defaultModuleTypeProvider`; the router is simply absent from that list.\n * If it is added, affected components bail out on their own.\n *\n * Caveat for the eventual move to the Rust/oxc React Compiler\n * (https://github.com/vitejs/vite/discussions/22949): switching compilers does\n * not by itself make render-time `matchRoute()` safe. It reimplements the same\n * memoization, so the same freeze applies unless it also ships the\n * module-type-provider bailout above. Re-verify rather than assume — compile a\n * consumer and check whether the `matchRoute(...)` call sits inside a memo gate\n * keyed only on stable values.\n */\n\nimport { definePlugin, defineRule } from '@oxlint/plugins';\nimport type { ESTree } from '@oxlint/plugins';\n\nconst ROUTER_MODULE = '@tanstack/react-router';\nconst MATCH_ROUTE_HOOK = 'useMatchRoute';\n\n/**\n * Hooks whose function argument still runs during render, so a `matchRoute()`\n * call inside one is memoized exactly like a bare render-time call.\n * `useCallback`/`useEffect` are absent on purpose — those defer past render.\n */\nconst RENDER_TIME_CALLBACK_HOOKS = new Set(['useMemo']);\n\ntype Callee = ESTree.CallExpression['callee'];\ntype FunctionNode = ESTree.Function | ESTree.ArrowFunctionExpression;\n\ntype Frame = {\n /** Names bound to a `useMatchRoute()` return value in this scope. */\n bindings: Set<string>;\n /** True when this frame's body executes during render. */\n runsDuringRender: boolean;\n};\n\nfunction isMatchRouteHookCall(\n node: ESTree.VariableDeclarator['init'] | Callee,\n hookNames: Set<string>,\n) {\n return (\n node?.type === 'CallExpression' &&\n node.callee.type === 'Identifier' &&\n hookNames.has(node.callee.name)\n );\n}\n\n/** Resolves the called name for both `useMemo(...)` and `React.useMemo(...)`. */\nfunction calleeName(callee: Callee) {\n if (callee.type === 'Identifier') return callee.name;\n if (\n callee.type === 'MemberExpression' &&\n !callee.computed &&\n callee.property.type === 'Identifier'\n ) {\n return callee.property.name;\n }\n return undefined;\n}\n\nconst rule = defineRule({\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Disallow calling a useMatchRoute() result during render; the React Compiler caches the result forever.',\n },\n messages: {\n renderTimeMatchRoute:\n 'Calling `{{name}}()` during render freezes the result — the React Compiler memoizes it on the ' +\n 'stable callback identity, so it never updates after navigation (TanStack/router#4499). ' +\n 'Evaluate the match inside a `useRouterState` selector, or move the call into an event ' +\n 'handler if you only need the match at event time.',\n },\n },\n create(context) {\n /** Local names the router's `useMatchRoute` is imported under. */\n const hookNames = new Set<string>();\n /** Function arguments of `useMemo`-style hooks, which still run at render. */\n const renderTimeCallbacks = new WeakSet<object>();\n const stack: Array<Frame> = [\n { bindings: new Set(), runsDuringRender: false },\n ];\n\n function enterFunction(node: FunctionNode) {\n stack.push({\n bindings: new Set(),\n runsDuringRender: renderTimeCallbacks.has(node),\n });\n }\n\n function exitFunction() {\n stack.pop();\n }\n\n return {\n ImportDeclaration(node) {\n if (node.source.value !== ROUTER_MODULE) return;\n\n for (const specifier of node.specifiers) {\n if (\n specifier.type === 'ImportSpecifier' &&\n specifier.imported.type === 'Identifier' &&\n specifier.imported.name === MATCH_ROUTE_HOOK\n ) {\n hookNames.add(specifier.local.name);\n }\n }\n },\n\n VariableDeclarator(node) {\n if (node.id.type !== 'Identifier') return;\n if (!isMatchRouteHookCall(node.init, hookNames)) return;\n\n stack.at(-1)?.bindings.add(node.id.name);\n },\n\n CallExpression(node) {\n // Record `useMemo(() => ..., deps)` callbacks before descending into them.\n const called = calleeName(node.callee);\n if (called !== undefined && RENDER_TIME_CALLBACK_HOOKS.has(called)) {\n const [callback] = node.arguments;\n if (\n callback?.type === 'ArrowFunctionExpression' ||\n callback?.type === 'FunctionExpression'\n ) {\n renderTimeCallbacks.add(callback);\n }\n }\n\n // `useMatchRoute()({ ... })` — invoked inline, always during render.\n if (isMatchRouteHookCall(node.callee, hookNames)) {\n context.report({\n node,\n messageId: 'renderTimeMatchRoute',\n data: { name: MATCH_ROUTE_HOOK },\n });\n return;\n }\n\n if (node.callee.type !== 'Identifier') return;\n const name = node.callee.name;\n\n // Innermost binding wins, so shadowed names resolve correctly.\n const declIndex = stack.findLastIndex((frame) =>\n frame.bindings.has(name),\n );\n if (declIndex === -1) return;\n\n // Render-time unless some frame between the declaration and the call\n // defers execution past render.\n const isRenderTime = stack\n .slice(declIndex + 1)\n .every((frame) => frame.runsDuringRender);\n if (!isRenderTime) return;\n\n context.report({\n node,\n messageId: 'renderTimeMatchRoute',\n data: { name },\n });\n },\n\n FunctionDeclaration: enterFunction,\n 'FunctionDeclaration:exit': exitFunction,\n FunctionExpression: enterFunction,\n 'FunctionExpression:exit': exitFunction,\n ArrowFunctionExpression: enterFunction,\n 'ArrowFunctionExpression:exit': exitFunction,\n };\n },\n});\n\nexport default definePlugin({\n meta: { name: 'route-active' },\n rules: { 'no-render-time-match-route': rule },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyDA,MAAM,gBAAgB;AACtB,MAAM,mBAAmB;;;;;;AAOzB,MAAM,6CAA6B,IAAI,IAAI,CAAC,SAAS,CAAC;AAYtD,SAAS,qBACP,MACA,WACA;CACA,OACE,MAAM,SAAS,oBACf,KAAK,OAAO,SAAS,gBACrB,UAAU,IAAI,KAAK,OAAO,IAAI;AAElC;;AAGA,SAAS,WAAW,QAAgB;CAClC,IAAI,OAAO,SAAS,cAAc,OAAO,OAAO;CAChD,IACE,OAAO,SAAS,sBAChB,CAAC,OAAO,YACR,OAAO,SAAS,SAAS,cAEzB,OAAO,OAAO,SAAS;AAG3B;AAEA,MAAM,OAAO,WAAW;CACtB,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,yGACJ;EACA,UAAU,EACR,sBACE,+TAIJ;CACF;CACA,OAAO,SAAS;;EAEd,MAAM,4BAAY,IAAI,IAAY;;EAElC,MAAM,sCAAsB,IAAI,QAAgB;EAChD,MAAM,QAAsB,CAC1B;GAAE,0BAAU,IAAI,IAAI;GAAG,kBAAkB;EAAM,CACjD;EAEA,SAAS,cAAc,MAAoB;GACzC,MAAM,KAAK;IACT,0BAAU,IAAI,IAAI;IAClB,kBAAkB,oBAAoB,IAAI,IAAI;GAChD,CAAC;EACH;EAEA,SAAS,eAAe;GACtB,MAAM,IAAI;EACZ;EAEA,OAAO;GACL,kBAAkB,MAAM;IACtB,IAAI,KAAK,OAAO,UAAU,eAAe;IAEzC,KAAK,MAAM,aAAa,KAAK,YAC3B,IACE,UAAU,SAAS,qBACnB,UAAU,SAAS,SAAS,gBAC5B,UAAU,SAAS,SAAS,kBAE5B,UAAU,IAAI,UAAU,MAAM,IAAI;GAGxC;GAEA,mBAAmB,MAAM;IACvB,IAAI,KAAK,GAAG,SAAS,cAAc;IACnC,IAAI,CAAC,qBAAqB,KAAK,MAAM,SAAS,GAAG;IAEjD,MAAM,GAAG,EAAE,CAAC,EAAE,SAAS,IAAI,KAAK,GAAG,IAAI;GACzC;GAEA,eAAe,MAAM;IAEnB,MAAM,SAAS,WAAW,KAAK,MAAM;IACrC,IAAI,WAAW,KAAA,KAAa,2BAA2B,IAAI,MAAM,GAAG;KAClE,MAAM,CAAC,YAAY,KAAK;KACxB,IACE,UAAU,SAAS,6BACnB,UAAU,SAAS,sBAEnB,oBAAoB,IAAI,QAAQ;IAEpC;IAGA,IAAI,qBAAqB,KAAK,QAAQ,SAAS,GAAG;KAChD,QAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM,EAAE,MAAM,iBAAiB;KACjC,CAAC;KACD;IACF;IAEA,IAAI,KAAK,OAAO,SAAS,cAAc;IACvC,MAAM,OAAO,KAAK,OAAO;IAGzB,MAAM,YAAY,MAAM,eAAe,UACrC,MAAM,SAAS,IAAI,IAAI,CACzB;IACA,IAAI,cAAc,IAAI;IAOtB,IAAI,CAHiB,MAClB,MAAM,YAAY,CAAC,CAAC,CACpB,OAAO,UAAU,MAAM,gBACV,GAAG;IAEnB,QAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM,EAAE,KAAK;IACf,CAAC;GACH;GAEA,qBAAqB;GACrB,4BAA4B;GAC5B,oBAAoB;GACpB,2BAA2B;GAC3B,yBAAyB;GACzB,gCAAgC;EAClC;CACF;AACF,CAAC;AAED,IAAA,qCAAe,aAAa;CAC1B,MAAM,EAAE,MAAM,eAAe;CAC7B,OAAO,EAAE,8BAA8B,KAAK;AAC9C,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-unexplained-dynamic-import.d.mts","names":[],"sources":["../src/no-unexplained-dynamic-import.ts"],"mappings":""}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { definePlugin, defineRule } from "@oxlint/plugins";
|
|
2
|
+
// src/no-unexplained-dynamic-import.ts
|
|
3
|
+
const reasonMarker = /\bdynamic-import\s+--\s+\S/u;
|
|
4
|
+
const isStatement = (node) => node.type.endsWith("Statement") || node.type === "VariableDeclaration" || node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration";
|
|
5
|
+
const enclosingStatement = (node) => {
|
|
6
|
+
let current = node;
|
|
7
|
+
while (!isStatement(current) && current.type !== "Program") current = current.parent;
|
|
8
|
+
return current;
|
|
9
|
+
};
|
|
10
|
+
const rule = defineRule({
|
|
11
|
+
meta: {
|
|
12
|
+
type: "suggestion",
|
|
13
|
+
docs: { description: "Require a dynamic-import -- reason comment for every dynamic import(); default to static imports." },
|
|
14
|
+
messages: { missingReason: "Dynamic import() needs a \"dynamic-import -- reason\" comment on the line above its statement, on the line above the import, or on the import itself, explaining why a static import will not do (code splitting, a lazy native module, an import cycle, an optional dependency)." }
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
const comments = context.sourceCode.getAllComments();
|
|
18
|
+
return { ImportExpression(node) {
|
|
19
|
+
const statement = enclosingStatement(node);
|
|
20
|
+
if (comments.some((comment) => reasonMarker.test(comment.value) && (comment.loc.end.line === statement.loc.start.line - 1 || comment.loc.end.line === node.loc.start.line - 1 || comment.loc.start.line <= node.loc.end.line && comment.loc.end.line >= node.loc.start.line))) return;
|
|
21
|
+
context.report({
|
|
22
|
+
node,
|
|
23
|
+
messageId: "missingReason"
|
|
24
|
+
});
|
|
25
|
+
} };
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
var no_unexplained_dynamic_import_default = definePlugin({
|
|
29
|
+
meta: { name: "dynamic-import" },
|
|
30
|
+
rules: { "no-unexplained-dynamic-import": rule }
|
|
31
|
+
});
|
|
32
|
+
export { no_unexplained_dynamic_import_default as default };
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=no-unexplained-dynamic-import.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-unexplained-dynamic-import.mjs","names":[],"sources":["../src/no-unexplained-dynamic-import.ts"],"sourcesContent":["// Requires every dynamic import() to carry a `dynamic-import -- reason`\n// comment. Static imports are the default: they keep the module graph visible\n// to bundlers, knip and typecheck, so a lazy load has to say why it is lazy\n// (code splitting, a lazy native module, an import cycle, an optional\n// dependency).\nimport { definePlugin, defineRule } from '@oxlint/plugins';\nimport type { ESTree } from '@oxlint/plugins';\n\nconst reasonMarker = /\\bdynamic-import\\s+--\\s+\\S/u;\n\nconst isStatement = (node: ESTree.Node) =>\n node.type.endsWith('Statement') ||\n node.type === 'VariableDeclaration' ||\n node.type === 'ExportNamedDeclaration' ||\n node.type === 'ExportDefaultDeclaration';\n\nconst enclosingStatement = (node: ESTree.ImportExpression) => {\n let current: ESTree.Node = node;\n while (!isStatement(current) && current.type !== 'Program') {\n current = current.parent;\n }\n return current;\n};\n\nconst rule = defineRule({\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Require a dynamic-import -- reason comment for every dynamic import(); default to static imports.',\n },\n messages: {\n missingReason:\n 'Dynamic import() needs a \"dynamic-import -- reason\" comment on the line above its statement, on ' +\n 'the line above the import, or on the import itself, explaining why a ' +\n 'static import will not do (code splitting, a lazy native module, an ' +\n 'import cycle, an optional dependency).',\n },\n },\n create(context) {\n const comments = context.sourceCode.getAllComments();\n\n return {\n ImportExpression(node) {\n const statement = enclosingStatement(node);\n const explained = comments.some(\n (comment) =>\n reasonMarker.test(comment.value) &&\n (comment.loc.end.line === statement.loc.start.line - 1 ||\n comment.loc.end.line === node.loc.start.line - 1 ||\n (comment.loc.start.line <= node.loc.end.line &&\n comment.loc.end.line >= node.loc.start.line)),\n );\n if (explained) return;\n context.report({ node, messageId: 'missingReason' });\n },\n };\n },\n});\n\nexport default definePlugin({\n meta: { name: 'dynamic-import' },\n rules: { 'no-unexplained-dynamic-import': rule },\n});\n"],"mappings":";;AAQA,MAAM,eAAe;AAErB,MAAM,eAAe,SACnB,KAAK,KAAK,SAAS,WAAW,KAC9B,KAAK,SAAS,yBACd,KAAK,SAAS,4BACd,KAAK,SAAS;AAEhB,MAAM,sBAAsB,SAAkC;CAC5D,IAAI,UAAuB;CAC3B,OAAO,CAAC,YAAY,OAAO,KAAK,QAAQ,SAAS,WAC/C,UAAU,QAAQ;CAEpB,OAAO;AACT;AAEA,MAAM,OAAO,WAAW;CACtB,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,oGACJ;EACA,UAAU,EACR,eACE,oRAIJ;CACF;CACA,OAAO,SAAS;EACd,MAAM,WAAW,QAAQ,WAAW,eAAe;EAEnD,OAAO,EACL,iBAAiB,MAAM;GACrB,MAAM,YAAY,mBAAmB,IAAI;GASzC,IARkB,SAAS,MACxB,YACC,aAAa,KAAK,QAAQ,KAAK,MAC9B,QAAQ,IAAI,IAAI,SAAS,UAAU,IAAI,MAAM,OAAO,KACnD,QAAQ,IAAI,IAAI,SAAS,KAAK,IAAI,MAAM,OAAO,KAC9C,QAAQ,IAAI,MAAM,QAAQ,KAAK,IAAI,IAAI,QACtC,QAAQ,IAAI,IAAI,QAAQ,KAAK,IAAI,MAAM,KAEnC,GAAG;GACf,QAAQ,OAAO;IAAE;IAAM,WAAW;GAAgB,CAAC;EACrD,EACF;CACF;AACF,CAAC;AAED,IAAA,wCAAe,aAAa;CAC1B,MAAM,EAAE,MAAM,iBAAiB;CAC/B,OAAO,EAAE,iCAAiC,KAAK;AACjD,CAAC"}
|