eslint-plugin-use-agnostic 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.
@@ -0,0 +1,212 @@
1
+ import path from "path";
2
+
3
+ import {
4
+ EXTENSIONS,
5
+ useServerJSXMessageId,
6
+ importBreaksEffectiveImportRulesMessageId,
7
+ reExportNotSameMessageId,
8
+ } from "../../_commons/constants/bases.js";
9
+ import {
10
+ USE_SERVER_LOGICS,
11
+ USE_SERVER_COMPONENTS,
12
+ USE_SERVER_FUNCTIONS,
13
+ USE_CLIENT_LOGICS,
14
+ USE_CLIENT_COMPONENTS,
15
+ USE_AGNOSTIC_LOGICS,
16
+ USE_AGNOSTIC_COMPONENTS,
17
+ // currentFileEffectiveDirective,
18
+ // importedFileEffectiveDirective,
19
+ effectiveDirectiveMessage,
20
+ specificViolationMessage,
21
+ } from "../constants/bases.js";
22
+
23
+ import { resolveImportPath } from "../../_commons/utilities/helpers.js";
24
+ import {
25
+ getDirectiveFromCurrentModule,
26
+ getDirectiveFromImportedModule,
27
+ getEffectiveDirective,
28
+ isImportBlocked,
29
+ makeMessageFromEffectiveDirective,
30
+ findSpecificViolationMessage,
31
+ } from "./helpers.js";
32
+
33
+ /* currentFileFlow */
34
+
35
+ /**
36
+ * 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.
37
+ * @param {Readonly<import('@typescript-eslint/utils').TSESLint.RuleContext<typeof reExportNotSameMessageId | typeof importBreaksEffectiveImportRulesMessageId | typeof useServerJSXMessageId, []>>} context The ESLint rule's `context` object.
38
+ * @returns {{skip: true; currentFileEffectiveDirective: undefined;} | {skip: undefined; currentFileEffectiveDirective: USE_SERVER_LOGICS | USE_SERVER_COMPONENTS | USE_SERVER_FUNCTIONS | USE_CLIENT_LOGICS | USE_CLIENT_COMPONENTS | USE_AGNOSTIC_LOGICS | USE_AGNOSTIC_COMPONENTS;}} Returns either an object with `skip: true` to disregard or one with the non-null `currentFileEffectiveDirective`.
39
+ */
40
+ export const currentFileFlow = (context) => {
41
+ // GETTING THE EXTENSION OF THE CURRENT FILE
42
+ const currentFileExtension = path.extname(context.filename);
43
+
44
+ // fails if the file is not JavaScript (TypeScript)
45
+ const iscurrentFileJS = EXTENSIONS.some(
46
+ (ext) => currentFileExtension === ext
47
+ );
48
+ if (!iscurrentFileJS) {
49
+ console.error(
50
+ "ERROR. Linted files for this rule should only be in JavaScript (TypeScript)."
51
+ );
52
+ return { skip: true };
53
+ }
54
+
55
+ /* GETTING THE DIRECTIVE (or lack thereof) OF THE CURRENT FILE */
56
+ const currentFileDirective = getDirectiveFromCurrentModule(context);
57
+
58
+ // reports if a file marked "use server" has a JSX extension
59
+ if (
60
+ currentFileDirective === "use server" &&
61
+ currentFileExtension.endsWith("x")
62
+ ) {
63
+ context.report({
64
+ loc: {
65
+ start: { line: 1, column: 0 },
66
+ end: { line: 1, column: context.sourceCode.lines[0].length },
67
+ },
68
+ messageId: useServerJSXMessageId,
69
+ });
70
+ return { skip: true };
71
+ }
72
+
73
+ // GETTING THE EFFECTIVE DIRECTIVE OF THE CURRENT FILE
74
+ const currentFileEffectiveDirective = getEffectiveDirective(
75
+ currentFileDirective,
76
+ currentFileExtension
77
+ );
78
+
79
+ // fails if one of the seven effective directives has not been obtained
80
+ if (currentFileEffectiveDirective === null) {
81
+ console.error("ERROR. Effective directive should never be null.");
82
+ return { skip: true };
83
+ }
84
+
85
+ return {
86
+ currentFileEffectiveDirective,
87
+ };
88
+ };
89
+
90
+ /* importedFileFlow */
91
+
92
+ /**
93
+ * The flow that is shared between import and re-export traversals to obtain the import file's effective directive.
94
+ * @param {string} currentDir Directory of the file containing the import (from `path.dirname(context.filename)`).
95
+ * @param {string} importPath The import specifier (e.g., `@/components/Button` or `./utils`).
96
+ * @param {string} cwd Project root (from `context.cwd`). Caveat: only as an assumption currently.
97
+ * @param {Readonly<import('@typescript-eslint/utils').TSESLint.RuleContext<typeof reExportNotSameMessageId | typeof importBreaksEffectiveImportRulesMessageId | typeof useServerJSXMessageId, []>>} context The ESLint rule's `context` object.
98
+ * @param {import('@typescript-eslint/types').TSESTree.ImportDeclaration} node The ESLint `node` of the rule's current traversal.
99
+ * @returns {{skip: true; importedFileEffectiveDirective: undefined; resolvedImportPath: undefined;} | {skip: undefined; importedFileEffectiveDirective: USE_SERVER_LOGICS | USE_SERVER_COMPONENTS | USE_SERVER_FUNCTIONS | USE_CLIENT_LOGICS | USE_CLIENT_COMPONENTS | USE_AGNOSTIC_LOGICS | USE_AGNOSTIC_COMPONENTS; resolvedImportPath: string;}} Returns either an object with `skip: true` to disregard or one with the non-null `importedFileEffectiveDirective`.
100
+ */
101
+ const importedFileFlow = (context, node) => {
102
+ // finds the full path of the import
103
+ const resolvedImportPath = resolveImportPath(
104
+ path.dirname(context.filename),
105
+ node.source.value,
106
+ context.cwd
107
+ );
108
+
109
+ // does not operate on paths it did not resolve
110
+ if (resolvedImportPath === null) return { skip: true };
111
+ // does not operate on non-JS files
112
+ const isImportedFileJS = EXTENSIONS.some((ext) =>
113
+ resolvedImportPath.endsWith(ext)
114
+ );
115
+ if (!isImportedFileJS) return { skip: true };
116
+
117
+ /* GETTING THE DIRECTIVE (or lack thereof) OF THE IMPORTED FILE */
118
+ const importedFileDirective =
119
+ getDirectiveFromImportedModule(resolvedImportPath);
120
+ // GETTING THE EXTENSION OF THE IMPORTED FILE
121
+ const importedFileFileExtension = path.extname(resolvedImportPath);
122
+ // GETTING THE EFFECTIVE DIRECTIVE OF THE IMPORTED FILE
123
+ const importedFileEffectiveDirective = getEffectiveDirective(
124
+ importedFileDirective,
125
+ importedFileFileExtension
126
+ );
127
+
128
+ // also fails if one of the seven effective directives has not been obtained
129
+ if (importedFileEffectiveDirective === null) {
130
+ console.error("ERROR. Effective directive should never be null.");
131
+ return { skip: true };
132
+ }
133
+
134
+ // For now skipping on both "does not operate" (which should ignore) and "fails" albeit with console.error (which should crash).
135
+
136
+ return {
137
+ importedFileEffectiveDirective,
138
+ };
139
+ };
140
+
141
+ /* importsFlow */
142
+
143
+ /** The full flow for import traversals to enforce effective directives import rules.
144
+ * @param {Readonly<import('@typescript-eslint/utils').TSESLint.RuleContext<typeof reExportNotSameMessageId | typeof importBreaksEffectiveImportRulesMessageId | typeof useServerJSXMessageId, []>>} context The ESLint rule's `context` object.
145
+ * @param {import('@typescript-eslint/types').TSESTree.ImportDeclaration} node The ESLint `node` of the rule's current traversal.
146
+ * @param {USE_SERVER_LOGICS | USE_SERVER_COMPONENTS | USE_SERVER_FUNCTIONS | USE_CLIENT_LOGICS | USE_CLIENT_COMPONENTS | USE_AGNOSTIC_LOGICS | USE_AGNOSTIC_COMPONENTS} currentFileEffectiveDirective The current file's effective directive.
147
+ * @returns Returns early if the flow needs to be interrupted.
148
+ */
149
+ export const importsFlow = (context, node, currentFileEffectiveDirective) => {
150
+ // does not operate on `import type`
151
+ if (node.importKind === "type") return;
152
+
153
+ const result = importedFileFlow(context, node);
154
+
155
+ if (result.skip) return;
156
+ const { importedFileEffectiveDirective } = result;
157
+
158
+ if (
159
+ isImportBlocked(
160
+ currentFileEffectiveDirective,
161
+ importedFileEffectiveDirective
162
+ )
163
+ ) {
164
+ context.report({
165
+ node,
166
+ messageId: importBreaksEffectiveImportRulesMessageId,
167
+ data: {
168
+ [effectiveDirectiveMessage]: makeMessageFromEffectiveDirective(
169
+ currentFileEffectiveDirective
170
+ ),
171
+ [specificViolationMessage]: findSpecificViolationMessage(
172
+ currentFileEffectiveDirective,
173
+ importedFileEffectiveDirective
174
+ ),
175
+ },
176
+ });
177
+ }
178
+ };
179
+
180
+ /* reExportsFlow */
181
+
182
+ /** The full flow for export traversals, shared between `ExportNamedDeclaration` and `ExportAllDeclaration`, to ensure same effective directive re-exports.
183
+ * @param {Readonly<import('@typescript-eslint/utils').TSESLint.RuleContext<typeof reExportNotSameMessageId | typeof importBreaksEffectiveImportRulesMessageId | typeof useServerJSXMessageId, []>>} context The ESLint rule's `context` object.
184
+ * @param {import('@typescript-eslint/types').TSESTree.ExportNamedDeclaration | import('@typescript-eslint/types').TSESTree.ExportAllDeclaration} node The ESLint `node` of the rule's current traversal.
185
+ * @param {USE_SERVER_LOGICS | USE_SERVER_COMPONENTS | USE_SERVER_FUNCTIONS | USE_CLIENT_LOGICS | USE_CLIENT_COMPONENTS | USE_AGNOSTIC_LOGICS | USE_AGNOSTIC_COMPONENTS} currentFileEffectiveDirective The current file's effective directive.
186
+ * @returns Returns early if the flow needs to be interrupted.
187
+ */
188
+ export const reExportsFlow = (context, node, currentFileEffectiveDirective) => {
189
+ // does not operate on `export type`
190
+ if (node.exportKind === "type") return;
191
+
192
+ // does not operate on internal exports
193
+ if (node.source === null) return;
194
+
195
+ const result = importedFileFlow(context, node);
196
+
197
+ if (result.skip) return;
198
+ const { importedFileEffectiveDirective } = result;
199
+
200
+ if (currentFileEffectiveDirective !== importedFileEffectiveDirective) {
201
+ context.report({
202
+ node,
203
+ messageId: reExportNotSameMessageId,
204
+ data: {
205
+ // currentFileEffectiveDirective
206
+ currentFileEffectiveDirective,
207
+ // importedFileEffectiveDirective
208
+ importedFileEffectiveDirective,
209
+ },
210
+ });
211
+ }
212
+ };
@@ -0,0 +1,186 @@
1
+ import {
2
+ useServerJSXMessageId,
3
+ importBreaksEffectiveImportRulesMessageId,
4
+ reExportNotSameMessageId,
5
+ TSX,
6
+ TS,
7
+ JSX,
8
+ JS,
9
+ MJS,
10
+ CJS,
11
+ } from "../../_commons/constants/bases.js";
12
+ import {
13
+ NO_DIRECTIVE,
14
+ USE_SERVER,
15
+ USE_CLIENT,
16
+ USE_AGNOSTIC,
17
+ USE_SERVER_LOGICS,
18
+ USE_SERVER_COMPONENTS,
19
+ USE_SERVER_FUNCTIONS,
20
+ USE_CLIENT_LOGICS,
21
+ USE_CLIENT_COMPONENTS,
22
+ USE_AGNOSTIC_LOGICS,
23
+ USE_AGNOSTIC_COMPONENTS,
24
+ effectiveDirectives_EffectiveModules,
25
+ directivesSet,
26
+ directivesArray,
27
+ effectiveDirectives_BlockedImports,
28
+ } from "../constants/bases.js";
29
+
30
+ import {
31
+ getImportedFileFirstLine,
32
+ isImportBlocked as commonsIsImportBlocked,
33
+ makeMessageFromResolvedDirective,
34
+ findSpecificViolationMessage as commonsFindSpecificViolationMessage,
35
+ } from "../../_commons/utilities/helpers.js";
36
+
37
+ /* getDirectiveFromCurrentModule */
38
+
39
+ /**
40
+ * Gets the directive of the current module.
41
+ * - `null` denotes a server-by-default module, ideally a Server Module.
42
+ * - `'use client'` denotes a Client Module.
43
+ * - `'use server'` denotes a Server Functions Module.
44
+ * - `'use agnostic'` denotes an Agnostic Module (formerly Shared Module).
45
+ * @param {Readonly<import('@typescript-eslint/utils').TSESLint.RuleContext<typeof reExportNotSameMessageId | typeof importBreaksEffectiveImportRulesMessageId | typeof useServerJSXMessageId, []>>} context The ESLint rule's `context` object.
46
+ * @returns {NO_DIRECTIVE | USE_SERVER | USE_CLIENT | USE_AGNOSTIC} The directive, or lack thereof via `null`. The lack of a directive is considered server-by-default.
47
+ */
48
+ export const getDirectiveFromCurrentModule = (context) => {
49
+ // the AST body to check for the top-of-the-file directive
50
+ const { body } = context.sourceCode.ast;
51
+
52
+ // the first statement from the source code's Abstract Syntax Tree
53
+ const firstStatement = body[0];
54
+
55
+ // the value of that first statement or null
56
+ const value =
57
+ firstStatement?.type === "ExpressionStatement" &&
58
+ firstStatement.expression?.type === "Literal"
59
+ ? firstStatement.expression.value
60
+ : null;
61
+
62
+ // considers early a null value as the absence of a directive
63
+ if (value === null) return value;
64
+
65
+ // 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
66
+ const currentFileDirective = directivesSet.has(value) ? value : null;
67
+
68
+ return currentFileDirective;
69
+ };
70
+
71
+ /* getEffectiveDirective */
72
+
73
+ /**
74
+ * Gets the effective directive of a module, based on the combination of its directive (or lack thereof) and its extension (depending on whether it ends with 'x' for JSX).
75
+ * - `'use server logics'` denotes a Server Logics Module.
76
+ * - `'use server components'` denotes a Server Components Module.
77
+ * - `'use server functions'` denotes a Server Functions Module.
78
+ * - `'use client logics'` denotes a Client Logics Module.
79
+ * - `'use client components'` denotes a Client Components Module.
80
+ * - `'use agnostic logics'` denotes an Agnostic Logics Module.
81
+ * - `'use agnostic components'` denotes an Agnostic Components Module.
82
+ * @param {NO_DIRECTIVE | USE_SERVER | USE_CLIENT | USE_AGNOSTIC} directive The directive as written on top of the file (`null` if no valid directive).
83
+ * @param {TSX | TS | JSX | JS | MJS | CJS} extension The JavaScript (TypeScript) extension of the file.
84
+ * @returns {USE_SERVER_LOGICS | USE_SERVER_COMPONENTS | USE_SERVER_FUNCTIONS | USE_CLIENT_LOGICS | USE_CLIENT_COMPONENTS | USE_AGNOSTIC_LOGICS | USE_AGNOSTIC_COMPONENTS | null} The effective directive, from which imports rules are applied.
85
+ */
86
+ export const getEffectiveDirective = (directive, extension) => {
87
+ // I could use a map, but because this is in JS with JSDoc, a manual solution is peculiarly more typesafe.
88
+ if (directive === NO_DIRECTIVE && !extension.endsWith("x"))
89
+ return USE_SERVER_LOGICS;
90
+ if (directive === NO_DIRECTIVE && extension.endsWith("x"))
91
+ return USE_SERVER_COMPONENTS;
92
+ if (directive === USE_SERVER && !extension.endsWith("x"))
93
+ return USE_SERVER_FUNCTIONS;
94
+ if (directive === USE_CLIENT && !extension.endsWith("x"))
95
+ return USE_CLIENT_LOGICS;
96
+ if (directive === USE_CLIENT && extension.endsWith("x"))
97
+ return USE_CLIENT_COMPONENTS;
98
+ if (directive === USE_AGNOSTIC && !extension.endsWith("x"))
99
+ return USE_AGNOSTIC_LOGICS;
100
+ if (directive === USE_AGNOSTIC && extension.endsWith("x"))
101
+ return USE_AGNOSTIC_COMPONENTS;
102
+
103
+ return null; // default error, should be unreachable
104
+ };
105
+
106
+ /* getDirectiveFromImportedModule */
107
+
108
+ /**
109
+ * Gets the directive of the imported module.
110
+ * - `'use client'` denotes a Client Module.
111
+ * - `'use server'` denotes a Server Functions Module.
112
+ * - `'use agnostic'` denotes an Agnostic Module (formerly Shared Module).
113
+ * - `null` denotes a server-by-default module, ideally a Server Module.
114
+ * @param {string} resolvedImportPath The resolved path of the import.
115
+ * @returns {USE_SERVER | USE_CLIENT | USE_AGNOSTIC | NO_DIRECTIVE} The directive, or lack thereof via `null`. The lack of a directive is considered server-by-default.
116
+ */
117
+ export const getDirectiveFromImportedModule = (resolvedImportPath) => {
118
+ // gets the first line of the code of the import
119
+ const importedFileFirstLine = getImportedFileFirstLine(resolvedImportPath);
120
+
121
+ // verifies that this first line starts with a valid directive, thus excluding comments
122
+ const hasAcceptedDirective = directivesArray.some(
123
+ (directive) =>
124
+ importedFileFirstLine.startsWith(`'${directive}'`) ||
125
+ importedFileFirstLine.startsWith(`"${directive}"`)
126
+ );
127
+
128
+ // applies the correct directive or the lack thereof with null
129
+ const importedFileDirective = hasAcceptedDirective
130
+ ? directivesArray.find((directive) =>
131
+ importedFileFirstLine.includes(directive)
132
+ ) ?? null
133
+ : null;
134
+
135
+ return importedFileDirective;
136
+ };
137
+
138
+ /* isImportBlocked */
139
+
140
+ /**
141
+ * Returns a boolean deciding if an imported file's effective directive is incompatible with the current file's effective directive.
142
+ * @param {USE_SERVER_LOGICS | USE_SERVER_COMPONENTS | USE_SERVER_FUNCTIONS | USE_CLIENT_LOGICS | USE_CLIENT_COMPONENTS | USE_AGNOSTIC_LOGICS | USE_AGNOSTIC_COMPONENTS} currentFileEffectiveDirective The current file's effective directive.
143
+ * @param {USE_SERVER_LOGICS | USE_SERVER_COMPONENTS | USE_SERVER_FUNCTIONS | USE_CLIENT_LOGICS | USE_CLIENT_COMPONENTS | USE_AGNOSTIC_LOGICS | USE_AGNOSTIC_COMPONENTS} importedFileEffectiveDirective The imported file's effective directive.
144
+ * @returns {boolean} Returns `true` if the import is blocked, as established in `effectiveDirectives_BlockedImports`.
145
+ */
146
+ export const isImportBlocked = (
147
+ currentFileEffectiveDirective,
148
+ importedFileEffectiveDirective
149
+ ) =>
150
+ commonsIsImportBlocked(
151
+ effectiveDirectives_BlockedImports,
152
+ currentFileEffectiveDirective,
153
+ importedFileEffectiveDirective
154
+ );
155
+
156
+ /* makeMessageFromEffectiveDirective */
157
+
158
+ /**
159
+ * Lists in an message the effective modules incompatible with an effective module based on its effective directive.
160
+ * @param {USE_SERVER_LOGICS | USE_SERVER_COMPONENTS | USE_SERVER_FUNCTIONS | USE_CLIENT_LOGICS | USE_CLIENT_COMPONENTS | USE_AGNOSTIC_LOGICS | USE_AGNOSTIC_COMPONENTS} effectiveDirective The effective directive of the effective module.
161
+ * @returns {string} The message listing the incompatible effective modules.
162
+ */
163
+ export const makeMessageFromEffectiveDirective = (effectiveDirective) =>
164
+ makeMessageFromResolvedDirective(
165
+ effectiveDirectives_EffectiveModules,
166
+ effectiveDirectives_BlockedImports,
167
+ effectiveDirective
168
+ );
169
+
170
+ /* findSpecificViolationMessage */
171
+
172
+ /**
173
+ * Finds the `message` for the specific violation of effective directives import rules based on `effectiveDirectives_BlockedImports`.
174
+ * @param {USE_SERVER_LOGICS | USE_SERVER_COMPONENTS | USE_SERVER_FUNCTIONS | USE_CLIENT_LOGICS | USE_CLIENT_COMPONENTS | USE_AGNOSTIC_LOGICS | USE_AGNOSTIC_COMPONENTS} currentFileEffectiveDirective The current file's effective directive.
175
+ * @param {USE_SERVER_LOGICS | USE_SERVER_COMPONENTS | USE_SERVER_FUNCTIONS | USE_CLIENT_LOGICS | USE_CLIENT_COMPONENTS | USE_AGNOSTIC_LOGICS | USE_AGNOSTIC_COMPONENTS} importedFileEffectiveDirective The imported file's effective directive.
176
+ * @returns {string} The corresponding `message`.
177
+ */
178
+ export const findSpecificViolationMessage = (
179
+ currentFileEffectiveDirective,
180
+ importedFileEffectiveDirective
181
+ ) =>
182
+ commonsFindSpecificViolationMessage(
183
+ effectiveDirectives_BlockedImports,
184
+ currentFileEffectiveDirective,
185
+ importedFileEffectiveDirective
186
+ );
@@ -0,0 +1,24 @@
1
+ import { defineConfig } from "eslint/config";
2
+
3
+ import {
4
+ directive21ConfigName,
5
+ useAgnosticPluginName,
6
+ enforceCommentedDirectivesRuleName,
7
+ } from "../_commons/constants/bases.js";
8
+
9
+ /**
10
+ * Makes the directive21 config for the use-agnostic ESLint plugin.
11
+ */
12
+ export const makeDirective21Config = (plugin) => ({
13
+ [directive21ConfigName]: defineConfig([
14
+ {
15
+ plugins: {
16
+ [useAgnosticPluginName]: plugin,
17
+ },
18
+ rules: {
19
+ [`${useAgnosticPluginName}/${enforceCommentedDirectivesRuleName}`]:
20
+ "warn",
21
+ },
22
+ },
23
+ ]),
24
+ });