@adonisjs/eslint-plugin 2.0.0 → 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.0",
4
+ "version": "2.1.0",
5
5
  "engines": {
6
6
  "node": ">=20.6.0"
7
7
  },
@@ -31,33 +31,36 @@
31
31
  "quick:test": "node --import=ts-node-maintained/register/esm --enable-source-maps bin/test.ts"
32
32
  },
33
33
  "devDependencies": {
34
- "@adonisjs/prettier-config": "^1.3.0",
35
- "@adonisjs/tsconfig": "^1.3.0",
36
- "@japa/assert": "^3.0.0",
37
- "@japa/runner": "^3.1.4",
38
- "@release-it/conventional-changelog": "^8.0.1",
39
- "@stylistic/eslint-plugin-ts": "^2.7.2",
40
- "@swc/core": "^1.7.21",
41
- "@types/node": "^22.5.1",
42
- "@typescript-eslint/rule-tester": "^8.3.0",
43
- "c8": "^10.1.2",
44
- "del-cli": "^5.1.0",
45
- "eslint": "^9.9.1",
46
- "eslint-config-prettier": "^9.1.0",
47
- "eslint-plugin-prettier": "^5.2.1",
48
- "eslint-plugin-unicorn": "^55.0.0",
49
- "prettier": "^3.3.3",
50
- "release-it": "^17.6.0",
51
- "ts-node-maintained": "^10.9.3",
52
- "tsup": "^8.2.4",
53
- "typescript": "^5.5.4",
54
- "typescript-eslint": "^8.3.0"
34
+ "@adonisjs/prettier-config": "^1.4.5",
35
+ "@adonisjs/tsconfig": "^1.4.1",
36
+ "@japa/assert": "^4.1.1",
37
+ "@japa/runner": "^4.3.0",
38
+ "@release-it/conventional-changelog": "^10.0.1",
39
+ "@stylistic/eslint-plugin-ts": "^4.4.1",
40
+ "@swc/core": "^1.13.3",
41
+ "@types/micromatch": "^4.0.10",
42
+ "@types/node": "^24.2.0",
43
+ "@typescript-eslint/rule-tester": "^8.39.0",
44
+ "c8": "^10.1.3",
45
+ "del-cli": "^6.0.0",
46
+ "eslint": "^9.32.0",
47
+ "eslint-config-prettier": "^10.1.8",
48
+ "eslint-plugin-prettier": "^5.5.3",
49
+ "eslint-plugin-unicorn": "^60.0.0",
50
+ "prettier": "^3.6.2",
51
+ "release-it": "^19.0.4",
52
+ "ts-node-maintained": "^10.9.6",
53
+ "tsup": "^8.5.0",
54
+ "typescript": "^5.9.2",
55
+ "typescript-eslint": "^8.39.0"
55
56
  },
56
57
  "peerDependencies": {
57
58
  "eslint": "^9.9.1"
58
59
  },
59
60
  "dependencies": {
60
- "@typescript-eslint/utils": "^8.3.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": {