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.
package/README.md ADDED
@@ -0,0 +1 @@
1
+ (In progress...)
@@ -0,0 +1,72 @@
1
+ // plugin name
2
+ export const useAgnosticPluginName = "use-agnostic";
3
+
4
+ /* config names */
5
+ // agnostic20
6
+ export const agnostic20ConfigName = "agnostic20";
7
+ // directive21
8
+ export const directive21ConfigName = "directive21";
9
+
10
+ /* rule names */
11
+ // agnostic20
12
+ export const enforceEffectiveDirectivesRuleName =
13
+ "enforce-effective-directives-import-rules";
14
+ // directive21
15
+ export const enforceCommentedDirectivesRuleName =
16
+ "enforce-commented-directives-import-rules";
17
+
18
+ /* messageIds */
19
+ export const reExportNotSameMessageId = "re-export-not-same-directive";
20
+ // agnostic20
21
+ export const importBreaksEffectiveImportRulesMessageId =
22
+ "import-breaks-effective-directive-import-rule";
23
+ export const useServerJSXMessageId = "use-server-has-jsx-extension";
24
+ // directive21
25
+ export const importBreaksCommentedImportRulesMessageId =
26
+ "import-breaks-commented-directive-import-rule";
27
+ export const noCommentedDirective = "no-commented-directive-detected";
28
+ export const commentedDirectiveVerificationFailed =
29
+ "commented-directive-verification-failed";
30
+ export const importNotStrategized =
31
+ "import-from-use-agnostic-strategies-not-strategized";
32
+ export const exportNotStrategized =
33
+ "export-from-use-agnostic-strategies-not-strategized";
34
+
35
+ // all "resolved" directives (from AIA/agnostic20 & DFA/directive21)
36
+ export const USE_SERVER_LOGICS = "use server logics";
37
+ export const USE_CLIENT_LOGICS = "use client logics";
38
+ export const USE_AGNOSTIC_LOGICS = "use agnostic logics";
39
+ export const USE_SERVER_COMPONENTS = "use server components";
40
+ export const USE_CLIENT_COMPONENTS = "use client components";
41
+ export const USE_AGNOSTIC_COMPONENTS = "use agnostic components";
42
+ export const USE_SERVER_FUNCTIONS = "use server functions";
43
+ export const USE_CLIENT_CONTEXTS = "use client contexts";
44
+ export const USE_AGNOSTIC_CONDITIONS = "use agnostic conditions";
45
+ export const USE_AGNOSTIC_STRATEGIES = "use agnostic strategies";
46
+
47
+ // all "resolved" modules (from AIA/agnostic20 & DFA/directive21)
48
+ export const SERVER_LOGICS_MODULE = "Server Logics Module";
49
+ export const CLIENT_LOGICS_MODULE = "Client Logics Module";
50
+ export const AGNOSTIC_LOGICS_MODULE = "Agnostic Logics Module";
51
+ export const SERVER_COMPONENTS_MODULE = "Server Components Module";
52
+ export const CLIENT_COMPONENTS_MODULE = "Client Components Module";
53
+ export const AGNOSTIC_COMPONENTS_MODULE = "Agnostic Components Module";
54
+ export const SERVER_FUNCTIONS_MODULE = "Server Functions Module";
55
+ export const CLIENT_CONTEXTS_MODULE = "Client Contexts Module";
56
+ export const AGNOSTIC_CONDITIONS_MODULE = "Agnostic Conditions Module";
57
+ export const AGNOSTIC_STRATEGIES_MODULE = "Agnostic Strategies Module";
58
+
59
+ /* from the resolveImportPath utility */
60
+
61
+ export const TSX = ".tsx";
62
+ export const TS = ".ts";
63
+ export const JSX = ".jsx";
64
+ export const JS = ".js";
65
+ export const MJS = ".mjs";
66
+ export const CJS = ".cjs";
67
+
68
+ export const EXTENSIONS = [TSX, TS, JSX, JS, MJS, CJS]; // In priority order
69
+
70
+ /* from the isImportBlocked utility */
71
+
72
+ export const ARE_NOT_ALLOWED_TO_IMPORT = "are not allowed to import";
@@ -0,0 +1,191 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ import { loadConfig, createMatchPath } from "tsconfig-paths";
5
+
6
+ import {
7
+ USE_SERVER_LOGICS,
8
+ USE_CLIENT_LOGICS,
9
+ USE_AGNOSTIC_LOGICS,
10
+ USE_SERVER_COMPONENTS,
11
+ USE_CLIENT_COMPONENTS,
12
+ USE_AGNOSTIC_COMPONENTS,
13
+ USE_SERVER_FUNCTIONS,
14
+ USE_CLIENT_CONTEXTS,
15
+ USE_AGNOSTIC_CONDITIONS,
16
+ USE_AGNOSTIC_STRATEGIES,
17
+ SERVER_LOGICS_MODULE,
18
+ CLIENT_LOGICS_MODULE,
19
+ AGNOSTIC_LOGICS_MODULE,
20
+ SERVER_COMPONENTS_MODULE,
21
+ CLIENT_COMPONENTS_MODULE,
22
+ AGNOSTIC_COMPONENTS_MODULE,
23
+ SERVER_FUNCTIONS_MODULE,
24
+ CLIENT_CONTEXTS_MODULE,
25
+ AGNOSTIC_CONDITIONS_MODULE,
26
+ AGNOSTIC_STRATEGIES_MODULE,
27
+ EXTENSIONS,
28
+ ARE_NOT_ALLOWED_TO_IMPORT,
29
+ } from "../constants/bases.js";
30
+
31
+ /* resolveImportPath */
32
+
33
+ /**
34
+ * Resolves an import path to a filesystem path, handling:
35
+ * - Aliases (via tsconfig.json `paths`)
36
+ * - Missing extensions (appends .ts, .tsx, etc.)
37
+ * - Directory imports (e.g., `./components` → `./components/index.ts`)
38
+ * @param {string} currentDir Directory of the file containing the import (from `path.dirname(context.filename)`).
39
+ * @param {string} importPath The import specifier (e.g., `@/components/Button` or `./utils`), from the current node.
40
+ * @param {string} cwd Project root (from `context.cwd`). Caveat: only as an assumption currently.
41
+ * @returns {string | null} Absolute resolved path or `null` if not found.
42
+ */
43
+ export const resolveImportPath = (currentDir, importPath, cwd) => {
44
+ // --- Step 1: Resolve aliases (if tsconfig.json `paths` exists) ---
45
+ const config = loadConfig(cwd);
46
+
47
+ const resolveTSConfig =
48
+ config.resultType === "success"
49
+ ? createMatchPath(config.absoluteBaseUrl, config.paths)
50
+ : null;
51
+
52
+ const aliasedPath = resolveTSConfig
53
+ ? resolveTSConfig(importPath, undefined, undefined, EXTENSIONS)
54
+ : null;
55
+
56
+ // --- Step 2: Resolve relative/absolute paths ---
57
+ const basePath = aliasedPath || path.resolve(currentDir, importPath);
58
+
59
+ // does not resolve on node_modules
60
+ if (basePath.includes("node_modules")) return null;
61
+
62
+ // Case 1: File with extension exists
63
+ if (path.extname(importPath) && fs.existsSync(basePath)) return basePath;
64
+
65
+ // Case 2: Try appending extensions
66
+ for (const ext of EXTENSIONS) {
67
+ const fullPath = `${basePath}${ext}`;
68
+ if (fs.existsSync(fullPath)) return fullPath;
69
+ }
70
+
71
+ // Case 3: Directory import (e.g., `./components` → `./components/index.ts`)
72
+ const indexPath = path.join(basePath, "index");
73
+ for (const ext of EXTENSIONS) {
74
+ const fullPath = `${indexPath}${ext}`;
75
+ if (fs.existsSync(fullPath)) return fullPath;
76
+ }
77
+
78
+ return null; // Not found
79
+ };
80
+
81
+ /* getImportedFileFirstLine */
82
+
83
+ /**
84
+ * Gets the first line of code of the imported module.
85
+ * @param {string} resolvedImportPath The resolved path of the imported module.
86
+ * @returns {string} Returns the first line of the imported module.
87
+ */
88
+ export const getImportedFileFirstLine = (resolvedImportPath) => {
89
+ // gets the code of the import
90
+ const importedFileContent = fs.readFileSync(resolvedImportPath, "utf8");
91
+ // gets the first line of the code of the import
92
+ const importedFileFirstLine = importedFileContent
93
+ .trim()
94
+ .split("\n")[0]
95
+ .trim(); // the line itself needs to be trimmed too
96
+
97
+ return importedFileFirstLine;
98
+ };
99
+
100
+ /* isImportBlocked */
101
+
102
+ /**
103
+ * Returns a boolean deciding if an imported file's "resolved" directive is incompatible with the current file's "resolved" directive.
104
+ * @param {Readonly<{"use server logics": {blockedImport: string; message: string;}[]; "use client logics": {blockedImport: string; message: string;}[]; "use agnostic logics": {blockedImport: string; message: string;}[]; "use server components": {blockedImport: string; message: string;}[]; "use client components": {blockedImport: string; message: string;}[]; "use agnostic components": {blockedImport: string; message: string;}[]; "use server functions": {blockedImport: string; message: string;}[]; "use client contexts"?: {blockedImport: string; message: string;}[]; "use agnostic conditions"?: {blockedImport: string; message: string;}[]; "use agnostic strategies"?: {blockedImport: string; message: string;}[];}>} resolvedDirectives_blockedImports The blocked imports object, either for agnostic20 or for directive21.
105
+ * @param {USE_SERVER_LOGICS | USE_CLIENT_LOGICS | USE_AGNOSTIC_LOGICS | USE_SERVER_COMPONENTS | USE_CLIENT_COMPONENTS | USE_AGNOSTIC_COMPONENTS | USE_SERVER_FUNCTIONS | USE_CLIENT_CONTEXTS | USE_AGNOSTIC_CONDITIONS | USE_AGNOSTIC_STRATEGIES} currentFileResolvedDirective The current file's "resolved" directive.
106
+ * @param {USE_SERVER_LOGICS | USE_CLIENT_LOGICS | USE_AGNOSTIC_LOGICS | USE_SERVER_COMPONENTS | USE_CLIENT_COMPONENTS | USE_AGNOSTIC_COMPONENTS | USE_SERVER_FUNCTIONS | USE_CLIENT_CONTEXTS | USE_AGNOSTIC_CONDITIONS} importedFileResolvedDirective The imported file's "resolved" directive.
107
+ * @returns {boolean} Returns `true` if the import is blocked, as established in respective `resolvedDirectives_blockedImports`.
108
+ */
109
+ export const isImportBlocked = (
110
+ // Note: "Blocked" here is preferred over "not allowed" because a specific message will be shared for each of the blocked situations, explaining their reasons and the solutions needed.
111
+ resolvedDirectives_blockedImports,
112
+ currentFileResolvedDirective,
113
+ importedFileResolvedDirective,
114
+ ) =>
115
+ resolvedDirectives_blockedImports[currentFileResolvedDirective]
116
+ .map((e) => e.blockedImport)
117
+ .includes(importedFileResolvedDirective);
118
+
119
+ /* makeIntroForSpecificViolationMessage */
120
+
121
+ /**
122
+ * Makes the intro for each specific import rule violation messages.
123
+ * @param {Readonly<{"use server logics": SERVER_LOGICS_MODULE; "use client logics": CLIENT_LOGICS_MODULE; "use agnostic logics": AGNOSTIC_LOGICS_MODULE; "use server components": SERVER_COMPONENTS_MODULE; "use client components": CLIENT_COMPONENTS_MODULE; "use agnostic components": AGNOSTIC_COMPONENTS_MODULE; "use server functions": SERVER_FUNCTIONS_MODULE; "use client contexts": CLIENT_CONTEXTS_MODULE; "use agnostic conditions": AGNOSTIC_CONDITIONS_MODULE; "use agnostic strategies": AGNOSTIC_STRATEGIES_MODULE;}>} resolvedDirectives_ResolvedModules The resolved modules object, either for agnostic20 or for directive21.
124
+ * @param {USE_SERVER_LOGICS | USE_CLIENT_LOGICS | USE_AGNOSTIC_LOGICS | USE_SERVER_COMPONENTS | USE_CLIENT_COMPONENTS | USE_AGNOSTIC_COMPONENTS | USE_SERVER_FUNCTIONS | USE_CLIENT_CONTEXTS | USE_AGNOSTIC_CONDITIONS | USE_AGNOSTIC_STRATEGIES} currentFileResolvedDirective The current file's "resolved" directive.
125
+ * @param {USE_SERVER_LOGICS | USE_CLIENT_LOGICS | USE_AGNOSTIC_LOGICS | USE_SERVER_COMPONENTS | USE_CLIENT_COMPONENTS | USE_AGNOSTIC_COMPONENTS | USE_SERVER_FUNCTIONS | USE_CLIENT_CONTEXTS | USE_AGNOSTIC_CONDITIONS} importedFileResolvedDirective The imported file's "resolved" directive.
126
+ * @returns {string} Returns "[Current file 'resolved' modules] are not allowed to import [imported file 'resolved' modules]".
127
+ */
128
+ export const makeIntroForSpecificViolationMessage = (
129
+ resolvedDirectives_ResolvedModules,
130
+ currentFileResolvedDirective,
131
+ importedFileResolvedDirective,
132
+ ) =>
133
+ `${resolvedDirectives_ResolvedModules[currentFileResolvedDirective]}s ${ARE_NOT_ALLOWED_TO_IMPORT} ${resolvedDirectives_ResolvedModules[importedFileResolvedDirective]}s.`;
134
+
135
+ /* makeMessageFromResolvedDirective */
136
+
137
+ /**
138
+ * Lists in an message the "resolved" modules incompatible with a "resolved" module based on its "resolved" directive.
139
+ * @param {Readonly<{"use server logics": SERVER_LOGICS_MODULE; "use client logics": CLIENT_LOGICS_MODULE; "use agnostic logics": AGNOSTIC_LOGICS_MODULE; "use server components": SERVER_COMPONENTS_MODULE; "use client components": CLIENT_COMPONENTS_MODULE; "use agnostic components": AGNOSTIC_COMPONENTS_MODULE; "use server functions": SERVER_FUNCTIONS_MODULE; "use client contexts": CLIENT_CONTEXTS_MODULE; "use agnostic conditions": AGNOSTIC_CONDITIONS_MODULE; "use agnostic strategies": AGNOSTIC_STRATEGIES_MODULE;}>} resolvedDirectives_ResolvedModules The resolved modules object, either for agnostic20 or for directive21.
140
+ * @param {Readonly<{"use server logics": {blockedImport: string; message: string;}[]; "use client logics": {blockedImport: string; message: string;}[]; "use agnostic logics": {blockedImport: string; message: string;}[]; "use server components": {blockedImport: string; message: string;}[]; "use client components": {blockedImport: string; message: string;}[]; "use agnostic components": {blockedImport: string; message: string;}[]; "use server functions": {blockedImport: string; message: string;}[]; "use client contexts"?: {blockedImport: string; message: string;}[]; "use agnostic conditions"?: {blockedImport: string; message: string;}[]; "use agnostic strategies"?: {blockedImport: string; message: string;}[];}>} resolvedDirectives_blockedImports The blocked imports object, either for agnostic20 or for directive21.
141
+ * @param {USE_SERVER_LOGICS | USE_CLIENT_LOGICS | USE_AGNOSTIC_LOGICS | USE_SERVER_COMPONENTS | USE_CLIENT_COMPONENTS | USE_AGNOSTIC_COMPONENTS | USE_SERVER_FUNCTIONS | USE_CLIENT_CONTEXTS | USE_AGNOSTIC_CONDITIONS | USE_AGNOSTIC_STRATEGIES} resolvedDirective The "resolved" directive of the "resolved" module.
142
+ * @returns {string} The message listing the incompatible "resolved" modules.
143
+ */
144
+ export const makeMessageFromResolvedDirective = (
145
+ resolvedDirectives_ResolvedModules,
146
+ resolvedDirectives_blockedImports,
147
+ resolvedDirective,
148
+ ) => {
149
+ const effectiveModule = resolvedDirectives_ResolvedModules[resolvedDirective];
150
+ const effectiveModulesString = effectiveModule + "s"; // plural
151
+
152
+ const blockedImports =
153
+ resolvedDirectives_blockedImports[resolvedDirective].map(
154
+ (e) => e.blockedImport,
155
+ ) || [];
156
+
157
+ if (blockedImports.length === 0) {
158
+ return `${effectiveModulesString} are not restricted from importing any modules.`;
159
+ }
160
+
161
+ const blockedEffectiveModules = blockedImports.map(
162
+ (e) => resolvedDirectives_ResolvedModules[e] + "s", // plural
163
+ );
164
+
165
+ const blockedEffectiveModulesString =
166
+ blockedEffectiveModules.length === 1
167
+ ? blockedEffectiveModules[0]
168
+ : blockedEffectiveModules.slice(0, -1).join(", ") +
169
+ ", or " +
170
+ blockedEffectiveModules.slice(-1);
171
+
172
+ return `${effectiveModulesString} ${ARE_NOT_ALLOWED_TO_IMPORT} ${blockedEffectiveModulesString}.`;
173
+ };
174
+
175
+ /* findSpecificViolationMessage */
176
+
177
+ /**
178
+ * Finds the `message` for the specific violation of "resolved" directives import rules based on `resolvedDirectives_blockedImports`.
179
+ * @param {Readonly<{"use server logics": {blockedImport: string; message: string;}[]; "use client logics": {blockedImport: string; message: string;}[]; "use agnostic logics": {blockedImport: string; message: string;}[]; "use server components": {blockedImport: string; message: string;}[]; "use client components": {blockedImport: string; message: string;}[]; "use agnostic components": {blockedImport: string; message: string;}[]; "use server functions": {blockedImport: string; message: string;}[]; "use client contexts"?: {blockedImport: string; message: string;}[]; "use agnostic conditions"?: {blockedImport: string; message: string;}[]; "use agnostic strategies"?: {blockedImport: string; message: string;}[];}>} resolvedDirectives_blockedImports The blocked imports object, either for agnostic20 or for directive21.
180
+ * @param {USE_SERVER_LOGICS | USE_CLIENT_LOGICS | USE_AGNOSTIC_LOGICS | USE_SERVER_COMPONENTS | USE_CLIENT_COMPONENTS | USE_AGNOSTIC_COMPONENTS | USE_SERVER_FUNCTIONS | USE_CLIENT_CONTEXTS | USE_AGNOSTIC_CONDITIONS | USE_AGNOSTIC_STRATEGIES} currentFileResolvedDirective The current file's "resolved" directive.
181
+ * @param {USE_SERVER_LOGICS | USE_CLIENT_LOGICS | USE_AGNOSTIC_LOGICS | USE_SERVER_COMPONENTS | USE_CLIENT_COMPONENTS | USE_AGNOSTIC_COMPONENTS | USE_SERVER_FUNCTIONS | USE_CLIENT_CONTEXTS | USE_AGNOSTIC_CONDITIONS} importedFileResolvedDirective The imported file's "resolved" directive.
182
+ * @returns {string} The corresponding `message`.
183
+ */
184
+ export const findSpecificViolationMessage = (
185
+ resolvedDirectives_blockedImports,
186
+ currentFileResolvedDirective,
187
+ importedFileResolvedDirective,
188
+ ) =>
189
+ resolvedDirectives_blockedImports[currentFileResolvedDirective].find(
190
+ (e) => e.blockedImport === importedFileResolvedDirective,
191
+ ).message;
@@ -0,0 +1,24 @@
1
+ import { defineConfig } from "eslint/config";
2
+
3
+ import {
4
+ agnostic20ConfigName,
5
+ useAgnosticPluginName,
6
+ enforceEffectiveDirectivesRuleName,
7
+ } from "../_commons/constants/bases.js";
8
+
9
+ /**
10
+ * Makes the agnostic20 config for the use-agnostic ESLint plugin.
11
+ */
12
+ export const makeAgnostic20Config = (plugin) => ({
13
+ [agnostic20ConfigName]: defineConfig([
14
+ {
15
+ plugins: {
16
+ [useAgnosticPluginName]: plugin,
17
+ },
18
+ rules: {
19
+ [`${useAgnosticPluginName}/${enforceEffectiveDirectivesRuleName}`]:
20
+ "warn",
21
+ },
22
+ },
23
+ ]),
24
+ });
@@ -0,0 +1,268 @@
1
+ import {
2
+ USE_SERVER_LOGICS as COMMONS_USE_SERVER_LOGICS,
3
+ USE_SERVER_COMPONENTS as COMMONS_USE_SERVER_COMPONENTS,
4
+ USE_SERVER_FUNCTIONS as COMMONS_USE_SERVER_FUNCTIONS,
5
+ USE_CLIENT_LOGICS as COMMONS_USE_CLIENT_LOGICS,
6
+ USE_CLIENT_COMPONENTS as COMMONS_USE_CLIENT_COMPONENTS,
7
+ USE_AGNOSTIC_LOGICS as COMMONS_USE_AGNOSTIC_LOGICS,
8
+ USE_AGNOSTIC_COMPONENTS as COMMONS_USE_AGNOSTIC_COMPONENTS,
9
+ SERVER_LOGICS_MODULE as COMMONS_SERVER_LOGICS_MODULE,
10
+ SERVER_COMPONENTS_MODULE as COMMONS_SERVER_COMPONENTS_MODULE,
11
+ SERVER_FUNCTIONS_MODULE as COMMONS_SERVER_FUNCTIONS_MODULE,
12
+ CLIENT_LOGICS_MODULE as COMMONS_CLIENT_LOGICS_MODULE,
13
+ CLIENT_COMPONENTS_MODULE as COMMONS_CLIENT_COMPONENTS_MODULE,
14
+ AGNOSTIC_LOGICS_MODULE as COMMONS_AGNOSTIC_LOGICS_MODULE,
15
+ AGNOSTIC_COMPONENTS_MODULE as COMMONS_AGNOSTIC_COMPONENTS_MODULE,
16
+ } from "../../_commons/constants/bases.js";
17
+
18
+ import { makeIntroForSpecificViolationMessage as commonsMakeIntroForSpecificViolationMessage } from "../../_commons/utilities/helpers.js";
19
+
20
+ // directives
21
+ export const NO_DIRECTIVE = null;
22
+ export const USE_SERVER = "use server";
23
+ export const USE_CLIENT = "use client";
24
+ export const USE_AGNOSTIC = "use agnostic";
25
+
26
+ // effective directives
27
+ export const USE_SERVER_LOGICS = COMMONS_USE_SERVER_LOGICS;
28
+ export const USE_SERVER_COMPONENTS = COMMONS_USE_SERVER_COMPONENTS;
29
+ export const USE_SERVER_FUNCTIONS = COMMONS_USE_SERVER_FUNCTIONS;
30
+ export const USE_CLIENT_LOGICS = COMMONS_USE_CLIENT_LOGICS;
31
+ export const USE_CLIENT_COMPONENTS = COMMONS_USE_CLIENT_COMPONENTS;
32
+ export const USE_AGNOSTIC_LOGICS = COMMONS_USE_AGNOSTIC_LOGICS;
33
+ export const USE_AGNOSTIC_COMPONENTS = COMMONS_USE_AGNOSTIC_COMPONENTS;
34
+
35
+ // effective modules
36
+ const SERVER_LOGICS_MODULE = COMMONS_SERVER_LOGICS_MODULE;
37
+ const SERVER_COMPONENTS_MODULE = COMMONS_SERVER_COMPONENTS_MODULE;
38
+ const SERVER_FUNCTIONS_MODULE = COMMONS_SERVER_FUNCTIONS_MODULE;
39
+ const CLIENT_LOGICS_MODULE = COMMONS_CLIENT_LOGICS_MODULE;
40
+ const CLIENT_COMPONENTS_MODULE = COMMONS_CLIENT_COMPONENTS_MODULE;
41
+ const AGNOSTIC_LOGICS_MODULE = COMMONS_AGNOSTIC_LOGICS_MODULE;
42
+ const AGNOSTIC_COMPONENTS_MODULE = COMMONS_AGNOSTIC_COMPONENTS_MODULE;
43
+
44
+ // mapping effective directives with effective modules
45
+ export const effectiveDirectives_EffectiveModules = Object.freeze({
46
+ [USE_SERVER_LOGICS]: SERVER_LOGICS_MODULE,
47
+ [USE_SERVER_COMPONENTS]: SERVER_COMPONENTS_MODULE,
48
+ [USE_SERVER_FUNCTIONS]: SERVER_FUNCTIONS_MODULE,
49
+ [USE_CLIENT_LOGICS]: CLIENT_LOGICS_MODULE,
50
+ [USE_CLIENT_COMPONENTS]: CLIENT_COMPONENTS_MODULE,
51
+ [USE_AGNOSTIC_LOGICS]: AGNOSTIC_LOGICS_MODULE,
52
+ [USE_AGNOSTIC_COMPONENTS]: AGNOSTIC_COMPONENTS_MODULE,
53
+ });
54
+
55
+ // message placeholders
56
+
57
+ export const currentFileEffectiveDirective = "currentFileEffectiveDirective";
58
+ export const importedFileEffectiveDirective = "importedFileEffectiveDirective";
59
+ export const effectiveDirectiveMessage = "effectiveDirectiveMessage";
60
+ export const specificViolationMessage = "specificViolationMessage";
61
+
62
+ /* from the getDirectiveFromCurrentModule utility */
63
+
64
+ export const directivesSet = new Set([USE_SERVER, USE_CLIENT, USE_AGNOSTIC]);
65
+
66
+ /* from the getDirectiveFromImportedModule utility */
67
+
68
+ export const directivesArray = Array.from(directivesSet);
69
+
70
+ /* from the isImportBlocked utility */
71
+
72
+ /* effectiveDirectives_BlockedImports */
73
+
74
+ /**
75
+ * Makes the intro for each specific import rule violation messages.
76
+ * @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.
77
+ * @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.
78
+ * @returns {string} Returns "[Current file effective modules] are not allowed to import [imported file effective modules]".
79
+ */
80
+ const makeIntroForSpecificViolationMessage = (
81
+ currentFileEffectiveDirective,
82
+ importedFileEffectiveDirective
83
+ ) =>
84
+ commonsMakeIntroForSpecificViolationMessage(
85
+ effectiveDirectives_EffectiveModules,
86
+ currentFileEffectiveDirective,
87
+ importedFileEffectiveDirective
88
+ );
89
+
90
+ export const effectiveDirectives_BlockedImports = Object.freeze({
91
+ [USE_SERVER_LOGICS]: [
92
+ // USE_SERVER_LOGICS allowed, because Server Logics can compose with one another.
93
+ // USE_SERVER_COMPONENTS allowed, because Server Components are OK to be composed with Server Logics as long as the Server Logics Module, by convention, does not export React components.
94
+ // USE_SERVER_FUNCTIONS allowed, because as Server Functions can import one another, it is only natural that they could compose through Server Logics.
95
+ {
96
+ blockedImport: USE_CLIENT_LOGICS,
97
+ message: `${makeIntroForSpecificViolationMessage(
98
+ USE_SERVER_LOGICS,
99
+ USE_CLIENT_LOGICS
100
+ )} Client logic should never leak to the server.`,
101
+ },
102
+ {
103
+ blockedImport: USE_CLIENT_COMPONENTS,
104
+ message: `${makeIntroForSpecificViolationMessage(
105
+ USE_SERVER_LOGICS,
106
+ USE_CLIENT_COMPONENTS
107
+ )} Client Components cannot be tinkered with on the server.`,
108
+ },
109
+ // USE_AGNOSTIC_LOGICS allowed, because Agnostic Logic can run safely on the server just like it can on the client.
110
+ // USE_AGNOSTIC_COMPONENTS allowed, because Agnostic Components can be composed with Logics on the server just like they can on the client, again, as long at the Server Logics Module, by convention, does not export React components.
111
+ ],
112
+ [USE_SERVER_COMPONENTS]: [
113
+ // USE_SERVER_LOGICS allowed, because logic from the server can safely support Server Components.
114
+ // USE_SERVER_COMPONENTS allowed, because Server Components can compose with one another, assuming thanks to the inclusion of the 'use agnostic' directive that they are actual Server Components.
115
+ // USE_SERVER_FUNCTIONS allowed, because as Server Components Modules can import Client Components, they are able to pass them Server Functions as props as well, even though indeed Server Components Modules can make their own Server Functions through inline 'use server' directives.
116
+ {
117
+ blockedImport: USE_CLIENT_LOGICS,
118
+ message: `${makeIntroForSpecificViolationMessage(
119
+ USE_SERVER_COMPONENTS,
120
+ USE_CLIENT_LOGICS
121
+ )} Client logic should never leak to the server.`,
122
+ },
123
+ // USE_CLIENT_COMPONENTS allowed, because Client Components can be nested inside Server Components either to wrap some of the tree with client state accessible through child Client Components and pass through Server Components, or to create client boundaries when the root of the application is planted on the server.
124
+ // USE_AGNOSTIC_LOGICS allowed, because Agnostic Logic can run safely on the server just like it can on the client.
125
+ // USE_AGNOSTIC_COMPONENTS allowed, because Agnostic Components can render safely on the server just like they can on the client.
126
+ ],
127
+ [USE_SERVER_FUNCTIONS]: [
128
+ // USE_SERVER_LOGICS allowed, because logic from the server can safely support Server Functions.
129
+ {
130
+ blockedImport: USE_SERVER_COMPONENTS,
131
+ message: `${makeIntroForSpecificViolationMessage(
132
+ USE_SERVER_FUNCTIONS,
133
+ USE_SERVER_COMPONENTS
134
+ )} Server Functions have no business working with React Components.`,
135
+ },
136
+ // USE_SERVER_FUNCTIONS allowed, because even though Server Functions don't need to import one another and the same results can be generated via Server Logic alone for the outcome of a single Server Function, a rational use case could be found for composing Server Functions with one another either today or in the future.
137
+ {
138
+ blockedImport: USE_CLIENT_LOGICS,
139
+ message: `${makeIntroForSpecificViolationMessage(
140
+ USE_SERVER_FUNCTIONS,
141
+ USE_CLIENT_LOGICS
142
+ )} Client logic should never leak to the server.`,
143
+ },
144
+ {
145
+ blockedImport: USE_CLIENT_COMPONENTS,
146
+ message: `${makeIntroForSpecificViolationMessage(
147
+ USE_SERVER_FUNCTIONS,
148
+ USE_CLIENT_COMPONENTS
149
+ )} Server Functions have no business working with React Components.`,
150
+ },
151
+ // USE_AGNOSTIC_LOGICS allowed, because Agnostic Logic can run safely on the server just like it can on the client.
152
+ {
153
+ blockedImport: USE_AGNOSTIC_COMPONENTS,
154
+ message: `${makeIntroForSpecificViolationMessage(
155
+ USE_SERVER_FUNCTIONS,
156
+ USE_AGNOSTIC_COMPONENTS
157
+ )} Server Functions have no business working with React Components.`,
158
+ },
159
+ ],
160
+ [USE_CLIENT_LOGICS]: [
161
+ {
162
+ blockedImport: USE_SERVER_LOGICS,
163
+ message: `${makeIntroForSpecificViolationMessage(
164
+ USE_CLIENT_LOGICS,
165
+ USE_SERVER_LOGICS
166
+ )} Server logic should never leak to the client.`,
167
+ },
168
+ {
169
+ blockedImport: USE_SERVER_COMPONENTS,
170
+ message: `${makeIntroForSpecificViolationMessage(
171
+ USE_CLIENT_LOGICS,
172
+ USE_SERVER_COMPONENTS
173
+ )} Server Components cannot be thinkered with on the client.`,
174
+ },
175
+ // USE_SERVER_FUNCTIONS allowed, because it is technically feasible to tinker with a Client Component within a Client Logics Module and attach to said Client Component a Server Function to be triggered on the client.
176
+ // USE_CLIENT_LOGICS allowed, because Client Logics can compose with one another.
177
+ // USE_CLIENT_COMPONENTS allowed, because Client Components are OK to be composed with Client Logics as long as the Client Logics Module, by convention, does not export React components.
178
+ // USE_AGNOSTIC_LOGICS allowed, because Agnostic Logic can run safely on the client just like it can on the server.
179
+ // USE_AGNOSTIC_COMPONENTS allowed, because Agnostic Components can be composed with Logics on the client just like they can on the server, again, as long at the Client Logics Module, by convention, does not export React components.
180
+ ],
181
+ [USE_CLIENT_COMPONENTS]: [
182
+ {
183
+ blockedImport: USE_SERVER_LOGICS,
184
+ message: `${makeIntroForSpecificViolationMessage(
185
+ USE_CLIENT_COMPONENTS,
186
+ USE_SERVER_LOGICS
187
+ )} Server logic should never leak to the client.`,
188
+ },
189
+ {
190
+ blockedImport: USE_SERVER_COMPONENTS,
191
+ message: `${makeIntroForSpecificViolationMessage(
192
+ USE_CLIENT_COMPONENTS,
193
+ USE_SERVER_COMPONENTS
194
+ )} Server Components may only pass through Client Components through the children prop within Server Components Modules.`,
195
+ },
196
+ // USE_SERVER_FUNCTIONS allowed, because Server Functions are specifically triggered by Client Components.
197
+ // USE_CLIENT_LOGICS allowed, because logic from the client can safely support Client Components.
198
+ // USE_CLIENT_COMPONENTS allowed, because Client Components can compose with one another.
199
+ // USE_AGNOSTIC_LOGICS allowed, because Agnostic Logic can run safely on the client just like it can on the server.
200
+ // USE_AGNOSTIC_COMPONENTS allowed, because Agnostic Components can render safely on the client just like they can on the server.
201
+ ],
202
+ [USE_AGNOSTIC_LOGICS]: [
203
+ {
204
+ blockedImport: USE_SERVER_LOGICS,
205
+ message: `${makeIntroForSpecificViolationMessage(
206
+ USE_AGNOSTIC_LOGICS,
207
+ USE_SERVER_LOGICS
208
+ )} Server Logic cannot run in both the server and the client.`,
209
+ },
210
+ {
211
+ blockedImport: USE_SERVER_COMPONENTS,
212
+ message: `${makeIntroForSpecificViolationMessage(
213
+ USE_AGNOSTIC_LOGICS,
214
+ USE_SERVER_COMPONENTS
215
+ )} Server Components cannot be tinkered with on both the server and the client.`,
216
+ },
217
+ {
218
+ blockedImport: USE_SERVER_FUNCTIONS,
219
+ message: `${makeIntroForSpecificViolationMessage(
220
+ USE_AGNOSTIC_LOGICS,
221
+ USE_SERVER_FUNCTIONS
222
+ )} Though Server Functions can be tinkered with on the server and on the client, use cases on both environments are not compatible.`,
223
+ },
224
+ {
225
+ blockedImport: USE_CLIENT_LOGICS,
226
+ message: `${makeIntroForSpecificViolationMessage(
227
+ USE_AGNOSTIC_LOGICS,
228
+ USE_CLIENT_LOGICS
229
+ )} Client Logic cannot run in both the server and the client.`,
230
+ },
231
+ {
232
+ blockedImport: USE_CLIENT_COMPONENTS,
233
+ message: `${makeIntroForSpecificViolationMessage(
234
+ USE_AGNOSTIC_LOGICS,
235
+ USE_CLIENT_COMPONENTS
236
+ )} Client Components cannot be tinkered with on both the server and the client.`,
237
+ },
238
+ // USE_AGNOSTIC_LOGICS allowed, because Agnostic Logics can compose with one another.
239
+ // USE_AGNOSTIC_COMPONENTS allowed, because Agnostic Components can be composed with Logics agnostically as long as the Agnostic Logics Module, by convention, does not export React components.
240
+ ],
241
+ [USE_AGNOSTIC_COMPONENTS]: [
242
+ {
243
+ blockedImport: USE_SERVER_LOGICS,
244
+ message: `${makeIntroForSpecificViolationMessage(
245
+ USE_AGNOSTIC_COMPONENTS,
246
+ USE_SERVER_LOGICS
247
+ )} Server Logic cannot run in both the server and the client.`,
248
+ },
249
+ {
250
+ blockedImport: USE_SERVER_COMPONENTS,
251
+ message: `${makeIntroForSpecificViolationMessage(
252
+ USE_AGNOSTIC_COMPONENTS,
253
+ USE_SERVER_COMPONENTS
254
+ )} Unlike Client Components, Server Components cannot make silos of their own once on the client, and can therefore not be executed from the client.`,
255
+ },
256
+ // USE_SERVER_FUNCTIONS allowed, because as Agnostic Components Modules can import Client Components, they are able to pass them Server Functions as props as well.
257
+ {
258
+ blockedImport: USE_CLIENT_LOGICS,
259
+ message: `${makeIntroForSpecificViolationMessage(
260
+ USE_AGNOSTIC_COMPONENTS,
261
+ USE_CLIENT_LOGICS
262
+ )} Client Logic cannot run in both the server and the client.`,
263
+ },
264
+ // USE_CLIENT_COMPONENTS allowed, because Client Components can be nested inside Agnostic Components either to wrap some of the tree with client state accessible through child Client Components and pass through Server Components (if still on the Server Tree), or to create client boundaries when the root of the application is planted on the server.
265
+ // USE_AGNOSTIC_LOGICS allowed, because environment-agnostic logic can safely support Agnostic Components.
266
+ // USE_AGNOSTIC_COMPONENTS allowed, because Agnostic Components can compose with one another.
267
+ ],
268
+ });
@@ -0,0 +1,55 @@
1
+ import {
2
+ reExportNotSameMessageId,
3
+ importBreaksEffectiveImportRulesMessageId,
4
+ useServerJSXMessageId,
5
+ } from "../../_commons/constants/bases.js";
6
+ import {
7
+ currentFileEffectiveDirective,
8
+ importedFileEffectiveDirective,
9
+ effectiveDirectiveMessage,
10
+ specificViolationMessage,
11
+ } from "../constants/bases.js";
12
+
13
+ import {
14
+ currentFileFlow,
15
+ importsFlow,
16
+ reExportsFlow,
17
+ } from "../utilities/flows.js";
18
+
19
+ /** @type {import('@typescript-eslint/utils').TSESLint.RuleModule<typeof reExportNotSameMessageId | typeof importBreaksEffectiveImportRulesMessageId | typeof useServerJSXMessageId, []>} */
20
+ const rule = {
21
+ meta: {
22
+ type: "problem",
23
+ docs: {
24
+ description:
25
+ "Enforces import rules based on the file's effective directive. ",
26
+ },
27
+ schema: [],
28
+ // currentFileEffectiveDirective, importedFileEffectiveDirective, effectiveDirectiveMessage, specificViolationMessage
29
+ messages: {
30
+ [reExportNotSameMessageId]: `The effective directives of this file and this re-export are dissimilar.
31
+ Here, "{{ ${currentFileEffectiveDirective} }}" and "{{ ${importedFileEffectiveDirective} }}" are not the same. Please re-export only from modules that have the same effective directive as the current module. `,
32
+ [importBreaksEffectiveImportRulesMessageId]: `{{ ${effectiveDirectiveMessage} }}
33
+ In this case, {{ ${specificViolationMessage} }} `,
34
+ [useServerJSXMessageId]: `Modules marked with the 'use server' directive are not allowed to have JSX file extensions.
35
+ Indeed, Server Functions Modules have no business exporting JSX. `,
36
+ },
37
+ },
38
+ create: (context) => {
39
+ const result = currentFileFlow(context);
40
+
41
+ if (result.skip) return {};
42
+ const { currentFileEffectiveDirective } = result;
43
+
44
+ return {
45
+ ImportDeclaration: (node) =>
46
+ importsFlow(context, node, currentFileEffectiveDirective),
47
+ ExportNamedDeclaration: (node) =>
48
+ reExportsFlow(context, node, currentFileEffectiveDirective),
49
+ ExportAllDeclaration: (node) =>
50
+ reExportsFlow(context, node, currentFileEffectiveDirective),
51
+ };
52
+ },
53
+ };
54
+
55
+ export default rule; // enforce-effective-directives-import-rules