eslint-plugin-use-agnostic 0.10.7 → 0.11.1

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
@@ -74,9 +74,7 @@ With this list established, it thus becomes possible to recognize static import
74
74
 
75
75
  ## Caveats
76
76
 
77
- Only the first line of code in a file is observed for the presence of a directive. If no top-of-the-file directive is present or recognized, the file is considered to not have a directive, defaulting to being understood as a Server Logics Module if it doesn't use a JSX file extension (`.js`, `.ts`) or as a Server Components Module if it does (`.jsx`, `.tsx`).
78
-
79
- Aliased import paths are resolved only if your ESLint config file and your `tsconfig.json` file are in the same directory. At least to my knowledge, since the resolution depends on the `cwd` property from ESLint rules' `context` objects.
77
+ Base url and aliased import paths are currently resolved under the assumption that `process.cwd()` and `context.cwd` have the same value.
80
78
 
81
79
  It is up to you to confirm that your Agnostic Modules are indeed agnostic, meaning that they have neither server- nor client-side code. `eslint-plugin-use-agnostic`, at least at this time, does not do this verification for you.
82
80
 
@@ -102,4 +100,4 @@ But not having a directive to distinguish between 1. non-special Server Modules
102
100
 
103
101
  This is what the `'use agnostic'` directive solves. It clearly marks a module to be an Agnostic Module. And if a module that used to lack a directive can now be marked as an Agnostic Module, this allows modules without a directive to finally, truly be Server Modules by default. And `eslint-plugin-use-agnostic` can work from there.
104
102
 
105
- A lot more needs to be done, and a lot of it unfortunately can only be optimized deeper into React's innerworkings. But if the introduction of `'use agnostic'` can already create such powerful static analysis, imagine what it could produce if only it were incorporated into React as an official directive of the Fullstack React Architecture.
103
+ A lot more needs to be done, and I suspect a lot of it unfortunately can only be optimized deeperly into React's innerworkings. But if the introduction of `'use agnostic'` can already create such powerful static analysis, imagine what it could produce if only it were incorporated into React as an official directive of the Fullstack React Architecture.
@@ -1,3 +1,6 @@
1
+ import { Linter } from "eslint";
2
+ import tseslint from "typescript-eslint";
3
+
1
4
  /**
2
5
  * @typedef {import('../../../types/_commons/typedefs').Extensions} Extensions
3
6
  */
@@ -117,3 +120,12 @@ export const ARE_NOT_ALLOWED_TO_IMPORT = "are not allowed to import";
117
120
  export const skip = Object.freeze({
118
121
  skip: true,
119
122
  });
123
+
124
+ // common linter for AST retrieval
125
+ export const linter = new Linter();
126
+
127
+ // ESLint configs language options
128
+ export const typeScriptCompatible = Object.freeze({
129
+ // for compatibility with .ts and .tsx
130
+ parser: tseslint.parser,
131
+ });
@@ -1,17 +1,20 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
 
4
+ import { Linter } from "eslint";
4
5
  import { loadConfig, createMatchPath } from "tsconfig-paths";
5
6
 
6
7
  import {
7
8
  EXTENSIONS,
8
9
  ARE_NOT_ALLOWED_TO_IMPORT,
9
10
  resolvedDirectives_resolvedModules,
11
+ // linter,
12
+ typeScriptCompatible,
10
13
  } from "../constants/bases.js";
11
14
 
12
15
  /**
13
- * @typedef {import('../../../types/_commons/typedefs').ResolvedDirectiveWithoutUseAgnosticStrategies} ResolvedDirectiveWithoutUseAgnosticStrategies
14
16
  * @typedef {import('../../../types/_commons/typedefs').Context<string, readonly unknown[]>} Context
17
+ * @typedef {import('../../../types/_commons/typedefs').ResolvedDirectiveWithoutUseAgnosticStrategies} ResolvedDirectiveWithoutUseAgnosticStrategies
15
18
  */
16
19
 
17
20
  /**
@@ -36,16 +39,16 @@ const findExistingPath = (basePath) => {
36
39
 
37
40
  /**
38
41
  * Resolves an import path to a filesystem path, handling:
39
- * - Aliases (via tsconfig.json `paths`)
40
- * - Missing extensions (appends .ts, .tsx, etc.)
42
+ * - Base url and aliases (via tsconfig.json `baseUrl` and `paths` compiler options)
43
+ * - Missing extensions (appends `.ts`, `.tsx`, etc.)
41
44
  * - Directory imports (e.g., `./components` → `./components/index.ts`)
42
45
  * @param {string} currentDir The directory of the file containing the import (from `path.dirname(context.filename)`).
43
46
  * @param {string} importPath The import specifier (e.g., `@/components/Button` or `./utils`), from the current node.
44
- * @param {string} cwd The project root (from `context.cwd`). Caveat: only as an assumption currently.
47
+ * @param {string} cwd The project root (from `context.cwd`).
45
48
  * @returns The absolute resolved path or `null` if no path is found.
46
49
  */
47
50
  export const resolveImportPath = (currentDir, importPath, cwd) => {
48
- // Step 1: Resolve baseUrl and aliases (if tsconfig.json `paths` exists)
51
+ // Step 1: Resolves baseUrl and aliases
49
52
  const config = loadConfig(cwd);
50
53
 
51
54
  const resolveTSConfig =
@@ -53,12 +56,12 @@ export const resolveImportPath = (currentDir, importPath, cwd) => {
53
56
  ? createMatchPath(config.absoluteBaseUrl, config.paths)
54
57
  : null;
55
58
 
56
- const aliasedPath = resolveTSConfig
59
+ const baseUrlOrAliasedPath = resolveTSConfig
57
60
  ? resolveTSConfig(importPath, undefined, undefined, EXTENSIONS)
58
61
  : null;
59
62
 
60
- // Step 2: Resolve relative/absolute paths
61
- const basePath = aliasedPath || path.resolve(currentDir, importPath);
63
+ // Step 2: Resolves relative/absolute paths
64
+ const basePath = baseUrlOrAliasedPath || path.resolve(currentDir, importPath);
62
65
 
63
66
  // does not resolve on node_modules
64
67
  if (basePath.includes("node_modules")) return null;
@@ -66,7 +69,7 @@ export const resolveImportPath = (currentDir, importPath, cwd) => {
66
69
  // Case 1: File with extension exists
67
70
  if (path.extname(importPath) && fs.existsSync(basePath)) return basePath;
68
71
 
69
- // Case 2: Try appending extensions
72
+ // Case 2: Tries appending extensions
70
73
  const extensionlessImportPath = findExistingPath(basePath);
71
74
  if (extensionlessImportPath) return extensionlessImportPath;
72
75
 
@@ -78,7 +81,30 @@ export const resolveImportPath = (currentDir, importPath, cwd) => {
78
81
  return null; // not found
79
82
  };
80
83
 
81
- /* getImportedFileFirstLine */
84
+ /* getASTFromResolvedPath */ // for agnostic20
85
+ // Note: For agnostic20, I need the AST so that the directive can be picked up on any line as long as it is the first statement of the file.
86
+
87
+ /**
88
+ * Gets the ESLint-generated Abstract Syntax Tree of a file from its resolved path.
89
+ * @param {string} resolvedPath The resolved path of the file.
90
+ * @returns The ESLint-generated AST (Abstract Syntax Tree) of the file.
91
+ */
92
+ export const getASTFromFilePath = (resolvedPath) => {
93
+ const linter = new Linter();
94
+ // the raw code of the file at the end of the resolved path
95
+ const text = fs.readFileSync(resolvedPath, "utf8");
96
+ // utilizes linter.verify ...
97
+ linter.verify(text, { languageOptions: typeScriptCompatible });
98
+ // ... to retrieve the raw code as a SourceCode object ...
99
+ const code = linter.getSourceCode();
100
+ // ... from which to extra the ESLint-generated AST
101
+ const ast = code.ast;
102
+
103
+ return ast;
104
+ };
105
+
106
+ /* getImportedFileFirstLine */ // for directive21
107
+ // Note: For directive21, I prioritize reading from the file system for performance, forgoing the retrieval of the source code comments for imported modules, since the Directive-First Architecture imposes that the first line of the file is reserved for its commented directive.
82
108
 
83
109
  /**
84
110
  * Gets the first line of code of the imported module.
@@ -114,9 +140,10 @@ export const highlightFirstLineOfCode = (context) => ({
114
140
  /**
115
141
  * Returns a boolean deciding if an imported file's "resolved" directive is incompatible with the current file's "resolved" directive.
116
142
  * @template {ResolvedDirectiveWithoutUseAgnosticStrategies} T
143
+ * @template {ResolvedDirectiveWithoutUseAgnosticStrategies} U
117
144
  * @param {ResolvedDirectives_BlockedImports<T>} resolvedDirectives_blockedImports The blocked imports object, either for agnostic20 or for directive21.
118
145
  * @param {T} currentFileResolvedDirective The current file's "resolved" directive.
119
- * @param {T} importedFileResolvedDirective The imported file's "resolved" directive.
146
+ * @param {U} importedFileResolvedDirective The imported file's "resolved" directive.
120
147
  * @returns `true` if the import is blocked, as established in respective `resolvedDirectives_blockedImports`.
121
148
  */
122
149
  export const isImportBlocked = (
@@ -135,8 +162,8 @@ export const isImportBlocked = (
135
162
  * Makes the intro for each specific import rule violation messages.
136
163
  * @template {ResolvedDirectiveWithoutUseAgnosticStrategies} T
137
164
  * @template {ResolvedDirectiveWithoutUseAgnosticStrategies} U
138
- * @param {T} currentFileResolvedDirective The current file's "resolved" directive, excluding `"use agnostic strategies"`.
139
- * @param {U} importedFileResolvedDirective The imported file's "resolved" directive, excluding `"use agnostic strategies"`.
165
+ * @param {T} currentFileResolvedDirective The current file's "resolved" directive.
166
+ * @param {U} importedFileResolvedDirective The imported file's "resolved" directive.
140
167
  * @returns "[Current file 'resolved' modules] are not allowed to import [imported file 'resolved' modules]."
141
168
  */
142
169
  export const makeIntroForSpecificViolationMessage = (
@@ -194,9 +221,10 @@ export const makeMessageFromCurrentFileResolvedDirective = (
194
221
  /**
195
222
  * Finds the `message` for the specific violation of "resolved" directives import rules based on `resolvedDirectives_blockedImports`.
196
223
  * @template {ResolvedDirectiveWithoutUseAgnosticStrategies} T
224
+ * @template {ResolvedDirectiveWithoutUseAgnosticStrategies} U
197
225
  * @param {ResolvedDirectives_BlockedImports<T>} resolvedDirectives_blockedImports The blocked imports object, either for agnostic20 or for directive21.
198
- * @param {T} currentFileResolvedDirective The current file's "resolved" directive, excluding `"use agnostic strategies"`.
199
- * @param {T} importedFileResolvedDirective The imported file's "resolved" directive.
226
+ * @param {T} currentFileResolvedDirective The current file's "resolved" directive.
227
+ * @param {U} importedFileResolvedDirective The imported file's "resolved" directive.
200
228
  * @returns The corresponding `message`.
201
229
  */
202
230
  export const findSpecificViolationMessage = (
@@ -54,28 +54,28 @@ export const FUNCTIONS = "functions";
54
54
 
55
55
  // mapping directives with effective directives
56
56
  /** @type {Directives_EffectiveDirectives} */
57
- export const directives_effectiveDirectives = {
58
- [NO_DIRECTIVE]: {
57
+ export const directives_effectiveDirectives = Object.freeze({
58
+ [NO_DIRECTIVE]: Object.freeze({
59
59
  [LOGICS]: USE_SERVER_LOGICS,
60
60
  [COMPONENTS]: USE_SERVER_COMPONENTS,
61
61
  [FUNCTIONS]: null,
62
- },
63
- [USE_SERVER]: {
62
+ }),
63
+ [USE_SERVER]: Object.freeze({
64
64
  [LOGICS]: null,
65
65
  [COMPONENTS]: null,
66
66
  [FUNCTIONS]: USE_SERVER_FUNCTIONS,
67
- },
68
- [USE_CLIENT]: {
67
+ }),
68
+ [USE_CLIENT]: Object.freeze({
69
69
  [LOGICS]: USE_CLIENT_LOGICS,
70
70
  [COMPONENTS]: USE_CLIENT_COMPONENTS,
71
71
  [FUNCTIONS]: null,
72
- },
73
- [USE_AGNOSTIC]: {
72
+ }),
73
+ [USE_AGNOSTIC]: Object.freeze({
74
74
  [LOGICS]: USE_AGNOSTIC_LOGICS,
75
75
  [COMPONENTS]: USE_AGNOSTIC_COMPONENTS,
76
76
  [FUNCTIONS]: null,
77
- },
78
- };
77
+ }),
78
+ });
79
79
 
80
80
  // message placeholders
81
81
  export const currentFileEffectiveDirective = "currentFileEffectiveDirective";
@@ -40,7 +40,7 @@ import {
40
40
  /* currentFileFlow */
41
41
 
42
42
  /**
43
- * The flow that begins the import rules enforcement rule, retrieving the valid directive of the current file before comparing it to upcoming valid directives of the files it imports.
43
+ * The flow that begins the import rules enforcement rule, retrieving the effective directive of the current file before comparing it to upcoming effective directives of the files it imports.
44
44
  * @param {Context} context The ESLint rule's `context` object.
45
45
  * @returns Either an object with `skip: true` to disregard or one with the non-null `currentFileEffectiveDirective`.
46
46
  */
@@ -9,13 +9,14 @@ import {
9
9
  } from "../constants/bases.js";
10
10
 
11
11
  import {
12
- getImportedFileFirstLine,
13
12
  isImportBlocked as commonsIsImportBlocked,
14
13
  makeMessageFromCurrentFileResolvedDirective,
15
14
  findSpecificViolationMessage as commonsFindSpecificViolationMessage,
15
+ getASTFromFilePath,
16
16
  } from "../../../_commons/utilities/helpers.js";
17
17
 
18
18
  /**
19
+ * @typedef {import('../../../../types/agnostic20/_commons/typedefs.js').AST} AST
19
20
  * @typedef {import('../../../../types/agnostic20/_commons/typedefs.js').Context} Context
20
21
  * @typedef {import('../../../../types/agnostic20/_commons/typedefs.js').Directive} Directive
21
22
  * @typedef {import('../../../../types/agnostic20/_commons/typedefs.js').NoDirective} NoDirective
@@ -23,20 +24,20 @@ import {
23
24
  * @typedef {import('../../../../types/agnostic20/_commons/typedefs.js').EffectiveDirective} EffectiveDirective
24
25
  */
25
26
 
26
- /* getDirectiveFromCurrentModule */
27
+ /* getDirectiveFromModule */
27
28
 
28
29
  /**
29
- * Gets the directive of the current module.
30
+ * Gets the directive of a module from its Abstract Syntax Tree.
30
31
  * - `null` denotes a server-by-default module, ideally a Server Module.
31
32
  * - `'use client'` denotes a Client Module.
32
33
  * - `'use server'` denotes a Server Functions Module.
33
34
  * - `'use agnostic'` denotes an Agnostic Module (formerly Shared Module).
34
- * @param {Context} context The ESLint rule's `context` object.
35
+ * @param {AST} ast The module's AST (Abstract Syntax Tree).
35
36
  * @returns The directive, or lack thereof via `null`. The lack of a directive is considered server-by-default.
36
37
  */
37
- export const getDirectiveFromCurrentModule = (context) => {
38
+ export const getDirectiveFromModule = (ast) => {
38
39
  // the AST body to check for the top-of-the-file directive
39
- const { body } = context.sourceCode.ast;
40
+ const { body } = ast;
40
41
 
41
42
  // the first statement from the source code's Abstract Syntax Tree
42
43
  const firstStatement = body[0];
@@ -52,10 +53,46 @@ export const getDirectiveFromCurrentModule = (context) => {
52
53
  if (value === null) return value;
53
54
 
54
55
  // the value to be exactly 'use client', 'use server' or 'use agnostic' in order not to be considered null by default, or server-by-default
55
- const currentFileDirective =
56
+ const moduleDirective =
56
57
  directivesArray.find((directive) => directive === value) ?? null;
57
58
 
58
- return currentFileDirective;
59
+ return moduleDirective;
60
+ };
61
+
62
+ /* getDirectiveFromCurrentModule */
63
+
64
+ /**
65
+ * Gets the directive of the current module.
66
+ * - `null` denotes a server-by-default module, ideally a Server Module.
67
+ * - `'use client'` denotes a Client Module.
68
+ * - `'use server'` denotes a Server Functions Module.
69
+ * - `'use agnostic'` denotes an Agnostic Module (formerly Shared Module).
70
+ * @param {Context} context The ESLint rule's `context` object.
71
+ * @returns The directive, or lack thereof via `null`. The lack of a directive is considered server-by-default.
72
+ */
73
+ export const getDirectiveFromCurrentModule = (context) => {
74
+ // the AST of the current module
75
+ const ast = context.sourceCode.ast;
76
+
77
+ return getDirectiveFromModule(ast);
78
+ };
79
+
80
+ /* getDirectiveFromImportedModule */
81
+
82
+ /**
83
+ * Gets the directive of the imported module.
84
+ * - `'use client'` denotes a Client Module.
85
+ * - `'use server'` denotes a Server Functions Module.
86
+ * - `'use agnostic'` denotes an Agnostic Module (formerly Shared Module).
87
+ * - `null` denotes a server-by-default module, ideally a Server Module.
88
+ * @param {string} resolvedImportPath The resolved path of the import.
89
+ * @returns The directive, or lack thereof via `null`. The lack of a directive is considered server-by-default.
90
+ */
91
+ export const getDirectiveFromImportedModule = (resolvedImportPath) => {
92
+ // the AST of the imported module
93
+ const ast = getASTFromFilePath(resolvedImportPath);
94
+
95
+ return getDirectiveFromModule(ast);
59
96
  };
60
97
 
61
98
  /* getEffectiveDirective */
@@ -83,38 +120,6 @@ export const getEffectiveDirective = (directive, extension) => {
83
120
  return directives_effectiveDirectives[directive][moduleKind];
84
121
  };
85
122
 
86
- /* getDirectiveFromImportedModule */
87
-
88
- /**
89
- * Gets the directive of the imported module.
90
- * - `'use client'` denotes a Client Module.
91
- * - `'use server'` denotes a Server Functions Module.
92
- * - `'use agnostic'` denotes an Agnostic Module (formerly Shared Module).
93
- * - `null` denotes a server-by-default module, ideally a Server Module.
94
- * @param {string} resolvedImportPath The resolved path of the import.
95
- * @returns The directive, or lack thereof via `null`. The lack of a directive is considered server-by-default.
96
- */
97
- export const getDirectiveFromImportedModule = (resolvedImportPath) => {
98
- // gets the first line of the code of the import
99
- const importedFileFirstLine = getImportedFileFirstLine(resolvedImportPath);
100
-
101
- // verifies that this first line starts with a valid directive, thus excluding comments
102
- const hasAcceptedDirective = directivesArray.some(
103
- (directive) =>
104
- importedFileFirstLine.startsWith(`'${directive}'`) ||
105
- importedFileFirstLine.startsWith(`"${directive}"`)
106
- );
107
-
108
- // applies the correct directive or the lack thereof with null
109
- const importedFileDirective = hasAcceptedDirective
110
- ? directivesArray.find((directive) =>
111
- importedFileFirstLine.includes(directive)
112
- ) ?? null
113
- : null;
114
-
115
- return importedFileDirective;
116
- };
117
-
118
123
  /* isImportBlocked */
119
124
 
120
125
  /**
@@ -1,10 +1,10 @@
1
1
  import { defineConfig } from "eslint/config";
2
- import tseslint from "typescript-eslint";
3
2
 
4
3
  import {
5
4
  agnostic20ConfigName,
6
5
  useAgnosticPluginName,
7
6
  enforceEffectiveDirectivesRuleName,
7
+ typeScriptCompatible,
8
8
  } from "../_commons/constants/bases.js";
9
9
 
10
10
  /**
@@ -26,10 +26,7 @@ export const makeAgnostic20Config = (plugin) => ({
26
26
  [`${useAgnosticPluginName}/${enforceEffectiveDirectivesRuleName}`]:
27
27
  "warn",
28
28
  },
29
- languageOptions: {
30
- // for compatibility with .ts and .tsx
31
- parser: tseslint.parser,
32
- },
29
+ languageOptions: typeScriptCompatible,
33
30
  },
34
31
  ]),
35
32
  });
@@ -11,10 +11,10 @@ import {
11
11
  } from "../constants/bases.js";
12
12
 
13
13
  import {
14
- getImportedFileFirstLine,
15
14
  isImportBlocked as commonsIsImportBlocked,
16
15
  makeMessageFromCurrentFileResolvedDirective,
17
16
  findSpecificViolationMessage as commonsFindSpecificViolationMessage,
17
+ getImportedFileFirstLine,
18
18
  } from "../../../_commons/utilities/helpers.js";
19
19
 
20
20
  /**
@@ -1,10 +1,10 @@
1
1
  import { defineConfig } from "eslint/config";
2
- import tseslint from "typescript-eslint";
3
2
 
4
3
  import {
5
4
  directive21ConfigName,
6
5
  useAgnosticPluginName,
7
6
  enforceCommentedDirectivesRuleName,
7
+ typeScriptCompatible,
8
8
  } from "../_commons/constants/bases.js";
9
9
 
10
10
  /**
@@ -26,10 +26,7 @@ export const makeDirective21Config = (plugin) => ({
26
26
  [`${useAgnosticPluginName}/${enforceCommentedDirectivesRuleName}`]:
27
27
  "warn",
28
28
  },
29
- languageOptions: {
30
- // for compatibility with .ts and .tsx
31
- parser: tseslint.parser,
32
- },
29
+ languageOptions: typeScriptCompatible,
33
30
  },
34
31
  ]),
35
32
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-use-agnostic",
3
- "version": "0.10.7",
3
+ "version": "0.11.1",
4
4
  "description": "Highlights problematic server-client imports in projects made with the Fullstack React Architecture.",
5
5
  "keywords": [
6
6
  "eslint",