@adonisjs/eslint-plugin 2.0.1 → 2.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
CHANGED
|
@@ -106,6 +106,85 @@ const SendVerificationEmail = () => import('#listeners/send_verification_email')
|
|
|
106
106
|
emitter.on('user:created', [SendVerificationEmail, 'handle'])
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
+
## `prefer-adonisjs-inertia-link`
|
|
110
|
+
|
|
111
|
+
> [!NOTE]
|
|
112
|
+
> This rule is for AdonisJS 7+ projects using `@adonisjs/inertia` v4+.
|
|
113
|
+
|
|
114
|
+
The `@adonisjs/prefer-adonisjs-inertia-link` rule warns when you import the `Link` component from `@inertiajs/react` or `@inertiajs/vue3` instead of using the typesafe version from `@adonisjs/inertia`.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
// ❌ Warning: Prefer importing Link from @adonisjs/inertia/react for typesafe routing
|
|
118
|
+
import { Link } from '@inertiajs/react'
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
// ✅ Correct
|
|
123
|
+
import { Link } from '@adonisjs/inertia/react'
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## `no-backend-import-in-frontend`
|
|
127
|
+
|
|
128
|
+
The `@adonisjs/no-backend-import-in-frontend` rule prevents importing backend code in your frontend files located in the `inertia/` directory.
|
|
129
|
+
|
|
130
|
+
The rule detects both:
|
|
131
|
+
|
|
132
|
+
- **Subpath imports** (`#models/user`) - automatically reads your `package.json` imports field
|
|
133
|
+
- **Relative imports** (`../../app/models/user`) - checks if the resolved path is outside `inertia/`
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
// inertia/pages/users.tsx
|
|
137
|
+
|
|
138
|
+
// ❌ Error: Importing backend code in frontend files is not allowed
|
|
139
|
+
import User from '#models/user'
|
|
140
|
+
import { UserService } from '../../app/services/user_service'
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
// inertia/pages/users.tsx
|
|
145
|
+
|
|
146
|
+
// ✅ Correct - type-only imports are allowed
|
|
147
|
+
import type { User } from '#models/user'
|
|
148
|
+
import type { UserService } from '../../app/services/user_service'
|
|
149
|
+
|
|
150
|
+
// ✅ Correct - imports pointing to inertia/ are allowed
|
|
151
|
+
import { Button } from '#components/button' // if #components/* -> ./inertia/components/*
|
|
152
|
+
import { utils } from '../utils'
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Sharing code between frontend and backend
|
|
156
|
+
|
|
157
|
+
If you have shared code (e.g., enums, constants, utility types) in your backend that you want to import in your frontend, you can use the `allowed` option to whitelist specific paths:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
// eslint.config.js
|
|
161
|
+
export default [
|
|
162
|
+
{
|
|
163
|
+
rules: {
|
|
164
|
+
'@adonisjs/no-backend-import-in-frontend': [
|
|
165
|
+
'error',
|
|
166
|
+
{
|
|
167
|
+
allowed: [
|
|
168
|
+
'#shared/*', // allows #shared/enums, #shared/constants, etc.
|
|
169
|
+
'#shared/**', // allows #shared/utils/helpers (deep nested)
|
|
170
|
+
'#enums', // exact match
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
]
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The `allowed` option uses [micromatch](https://github.com/micromatch/micromatch) for glob pattern matching.
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
// inertia/pages/users.tsx
|
|
183
|
+
|
|
184
|
+
// ✅ Correct - #shared/* is in the allowed list
|
|
185
|
+
import { UserStatus } from '#shared/enums'
|
|
186
|
+
```
|
|
187
|
+
|
|
109
188
|
<div align="center">
|
|
110
189
|
<sub>Built with ❤︎ by <a href="https://github.com/Julien-R44">Julien Ripouteau</a> and <a href="https://github.com/thetutlage">Harminder Virk</a>
|
|
111
190
|
</div>
|
package/build/index.d.ts
CHANGED
|
@@ -6,6 +6,14 @@ declare const _default: {
|
|
|
6
6
|
'prefer-lazy-listener-import': import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferLazyListenerImport", [], {
|
|
7
7
|
description: string;
|
|
8
8
|
}, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
9
|
+
'prefer-adonisjs-inertia-link': import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferAdonisInertiaLink", [], {
|
|
10
|
+
description: string;
|
|
11
|
+
}, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
12
|
+
'no-backend-import-in-frontend': import("@typescript-eslint/utils/ts-eslint").RuleModule<"noBackendImport", [{
|
|
13
|
+
allowed?: string[];
|
|
14
|
+
}], {
|
|
15
|
+
description: string;
|
|
16
|
+
}, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
9
17
|
};
|
|
10
18
|
};
|
|
11
19
|
export default _default;
|
package/build/index.js
CHANGED
|
@@ -151,11 +151,144 @@ var prefer_lazy_controller_import_default = createEslintRule({
|
|
|
151
151
|
}
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
+
// src/rules/prefer_adonisjs_inertia_link.ts
|
|
155
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES3 } from "@typescript-eslint/utils";
|
|
156
|
+
var INERTIA_PACKAGES = ["@inertiajs/react", "@inertiajs/vue3"];
|
|
157
|
+
var prefer_adonisjs_inertia_link_default = createEslintRule({
|
|
158
|
+
name: "prefer-adonisjs-inertia-link",
|
|
159
|
+
defaultOptions: [],
|
|
160
|
+
meta: {
|
|
161
|
+
type: "suggestion",
|
|
162
|
+
docs: {
|
|
163
|
+
description: "Prefer the typesafe @adonisjs/inertia Link component over the @inertiajs Link component"
|
|
164
|
+
},
|
|
165
|
+
schema: [],
|
|
166
|
+
messages: {
|
|
167
|
+
preferAdonisInertiaLink: "Prefer importing Link from @adonisjs/inertia/{{ framework }} for typesafe routing instead of {{ source }}"
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
create: function(context) {
|
|
171
|
+
return {
|
|
172
|
+
ImportDeclaration(node) {
|
|
173
|
+
const source = node.source.value;
|
|
174
|
+
if (!INERTIA_PACKAGES.includes(source)) return;
|
|
175
|
+
const hasLinkImport = node.specifiers.some((specifier) => {
|
|
176
|
+
if (specifier.type !== AST_NODE_TYPES3.ImportSpecifier) return false;
|
|
177
|
+
return specifier.imported.type === AST_NODE_TYPES3.Identifier && specifier.imported.name === "Link";
|
|
178
|
+
});
|
|
179
|
+
if (!hasLinkImport) return;
|
|
180
|
+
const framework = source === "@inertiajs/react" ? "react" : "vue";
|
|
181
|
+
context.report({
|
|
182
|
+
node,
|
|
183
|
+
messageId: "preferAdonisInertiaLink",
|
|
184
|
+
data: { framework, source }
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// src/rules/no_backend_import_in_frontend.ts
|
|
192
|
+
import micromatch from "micromatch";
|
|
193
|
+
import { dirname, resolve } from "path";
|
|
194
|
+
import { readPackageUpSync } from "read-package-up";
|
|
195
|
+
var packageCache = /* @__PURE__ */ new Map();
|
|
196
|
+
function getSubpathImports(filename) {
|
|
197
|
+
const cwd = dirname(filename);
|
|
198
|
+
if (packageCache.has(cwd)) return packageCache.get(cwd).imports;
|
|
199
|
+
const result = readPackageUpSync({ cwd, normalize: false });
|
|
200
|
+
if (!result) {
|
|
201
|
+
packageCache.set(cwd, { imports: null });
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
const imports = result.packageJson.imports ?? null;
|
|
205
|
+
packageCache.set(cwd, { imports });
|
|
206
|
+
return imports;
|
|
207
|
+
}
|
|
208
|
+
function resolveImportTarget(target) {
|
|
209
|
+
if (typeof target === "string") return [target];
|
|
210
|
+
if (Array.isArray(target)) return target;
|
|
211
|
+
if (typeof target === "object") return Object.values(target).flatMap(resolveImportTarget);
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
function subpathResolvesToFrontend(options) {
|
|
215
|
+
const { importPath, subpathImports } = options;
|
|
216
|
+
for (const [pattern, target] of Object.entries(subpathImports)) {
|
|
217
|
+
const patternBase = pattern.replace("/*", "").replace("*", "");
|
|
218
|
+
const importBase = importPath.replace("/*", "").replace("*", "");
|
|
219
|
+
if (importPath === pattern || importBase.startsWith(patternBase)) {
|
|
220
|
+
const resolvedPaths = resolveImportTarget(target);
|
|
221
|
+
const isFrontend = resolvedPaths.some((resolved) => {
|
|
222
|
+
return resolved.startsWith("./inertia/") || resolved.startsWith("inertia/");
|
|
223
|
+
});
|
|
224
|
+
if (isFrontend) return true;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
function relativePathResolvesToFrontend(options) {
|
|
230
|
+
const { importPath, filename } = options;
|
|
231
|
+
const absolutePath = resolve(dirname(filename), importPath);
|
|
232
|
+
return /[\\/]inertia[\\/]/.test(absolutePath);
|
|
233
|
+
}
|
|
234
|
+
var no_backend_import_in_frontend_default = createEslintRule({
|
|
235
|
+
name: "no-backend-import-in-frontend",
|
|
236
|
+
defaultOptions: [{ allowed: [] }],
|
|
237
|
+
meta: {
|
|
238
|
+
type: "problem",
|
|
239
|
+
docs: {
|
|
240
|
+
description: "Disallow importing backend code in frontend (Inertia) files"
|
|
241
|
+
},
|
|
242
|
+
schema: [
|
|
243
|
+
{
|
|
244
|
+
type: "object",
|
|
245
|
+
properties: {
|
|
246
|
+
allowed: {
|
|
247
|
+
type: "array",
|
|
248
|
+
items: { type: "string" },
|
|
249
|
+
description: 'List of allowed import paths or glob patterns (e.g. "#shared/*", "#enums")'
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
additionalProperties: false
|
|
253
|
+
}
|
|
254
|
+
],
|
|
255
|
+
messages: {
|
|
256
|
+
noBackendImport: 'Importing backend code "{{ importPath }}" in frontend files is not allowed. Use `import type` for type-only imports, or add the path to the `allowed` option.'
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
create: function(context, options) {
|
|
260
|
+
const filename = context.filename;
|
|
261
|
+
const allowed = options[0]?.allowed ?? [];
|
|
262
|
+
const isInInertiaFolder = /[\\/]inertia[\\/]/.test(filename);
|
|
263
|
+
if (!isInInertiaFolder) return {};
|
|
264
|
+
const subpathImports = getSubpathImports(filename);
|
|
265
|
+
return {
|
|
266
|
+
ImportDeclaration(node) {
|
|
267
|
+
const importPath = node.source.value;
|
|
268
|
+
if (node.importKind === "type") return;
|
|
269
|
+
if (allowed.length > 0 && micromatch.isMatch(importPath, allowed)) return;
|
|
270
|
+
if (importPath.startsWith("#")) {
|
|
271
|
+
if (!subpathImports) return;
|
|
272
|
+
if (subpathResolvesToFrontend({ importPath, subpathImports })) return;
|
|
273
|
+
context.report({ node, messageId: "noBackendImport", data: { importPath } });
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (importPath.startsWith(".")) {
|
|
277
|
+
if (relativePathResolvesToFrontend({ importPath, filename })) return;
|
|
278
|
+
context.report({ node, messageId: "noBackendImport", data: { importPath } });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
154
285
|
// index.ts
|
|
155
286
|
var index_default = {
|
|
156
287
|
rules: {
|
|
157
288
|
"prefer-lazy-controller-import": prefer_lazy_controller_import_default,
|
|
158
|
-
"prefer-lazy-listener-import": prefer_lazy_listener_import_default
|
|
289
|
+
"prefer-lazy-listener-import": prefer_lazy_listener_import_default,
|
|
290
|
+
"prefer-adonisjs-inertia-link": prefer_adonisjs_inertia_link_default,
|
|
291
|
+
"no-backend-import-in-frontend": no_backend_import_in_frontend_default
|
|
159
292
|
}
|
|
160
293
|
};
|
|
161
294
|
export {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type Options = [{
|
|
2
|
+
allowed?: string[];
|
|
3
|
+
}];
|
|
4
|
+
/**
|
|
5
|
+
* ESLint rule to prevent importing backend code in frontend files.
|
|
6
|
+
* Only applies to files in the `inertia/` directory.
|
|
7
|
+
* Automatically detects frontend subpath imports by reading the package.json imports field.
|
|
8
|
+
*/
|
|
9
|
+
declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"noBackendImport", Options, {
|
|
10
|
+
description: string;
|
|
11
|
+
}, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
12
|
+
export default _default;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule to prefer the typesafe AdonisJS Inertia Link component
|
|
3
|
+
* over the non-typesafe Inertia.js Link component
|
|
4
|
+
*/
|
|
5
|
+
declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferAdonisInertiaLink", [], {
|
|
6
|
+
description: string;
|
|
7
|
+
}, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
8
|
+
export default _default;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonisjs/eslint-plugin",
|
|
3
3
|
"description": "ESLint plugin to enforce AdonisJS app specific linting rules",
|
|
4
|
-
"version": "2.0
|
|
4
|
+
"version": "2.1.0",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.6.0"
|
|
7
7
|
},
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"@release-it/conventional-changelog": "^10.0.1",
|
|
39
39
|
"@stylistic/eslint-plugin-ts": "^4.4.1",
|
|
40
40
|
"@swc/core": "^1.13.3",
|
|
41
|
+
"@types/micromatch": "^4.0.10",
|
|
41
42
|
"@types/node": "^24.2.0",
|
|
42
43
|
"@typescript-eslint/rule-tester": "^8.39.0",
|
|
43
44
|
"c8": "^10.1.3",
|
|
@@ -57,7 +58,9 @@
|
|
|
57
58
|
"eslint": "^9.9.1"
|
|
58
59
|
},
|
|
59
60
|
"dependencies": {
|
|
60
|
-
"@typescript-eslint/utils": "^8.39.0"
|
|
61
|
+
"@typescript-eslint/utils": "^8.39.0",
|
|
62
|
+
"micromatch": "^4.0.8",
|
|
63
|
+
"read-package-up": "^12.0.0"
|
|
61
64
|
},
|
|
62
65
|
"homepage": "https://github.com/adonisjs/eslint-plugin-adonisjs#readme",
|
|
63
66
|
"repository": {
|