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 +1 -0
- package/library/_commons/constants/bases.js +72 -0
- package/library/_commons/utilities/helpers.js +191 -0
- package/library/agnostic20/config.js +24 -0
- package/library/agnostic20/constants/bases.js +268 -0
- package/library/agnostic20/rules/import-rules-enforcement.js +55 -0
- package/library/agnostic20/utilities/flows.js +212 -0
- package/library/agnostic20/utilities/helpers.js +186 -0
- package/library/directive21/config.js +24 -0
- package/library/directive21/constants/bases.js +460 -0
- package/library/directive21/rules/import-rules-enforcement.js +67 -0
- package/library/directive21/utilities/flows.js +268 -0
- package/library/directive21/utilities/helpers.js +291 -0
- package/library/index.js +42 -0
- package/package.json +43 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
EXTENSIONS,
|
|
5
|
+
reExportNotSameMessageId,
|
|
6
|
+
importBreaksCommentedImportRulesMessageId,
|
|
7
|
+
noCommentedDirective,
|
|
8
|
+
commentedDirectiveVerificationFailed,
|
|
9
|
+
importNotStrategized,
|
|
10
|
+
exportNotStrategized,
|
|
11
|
+
} from "../../_commons/constants/bases.js";
|
|
12
|
+
import {
|
|
13
|
+
USE_SERVER_LOGICS,
|
|
14
|
+
USE_CLIENT_LOGICS,
|
|
15
|
+
USE_AGNOSTIC_LOGICS,
|
|
16
|
+
USE_SERVER_COMPONENTS,
|
|
17
|
+
USE_CLIENT_COMPONENTS,
|
|
18
|
+
USE_AGNOSTIC_COMPONENTS,
|
|
19
|
+
USE_SERVER_FUNCTIONS,
|
|
20
|
+
USE_CLIENT_CONTEXTS,
|
|
21
|
+
USE_AGNOSTIC_CONDITIONS,
|
|
22
|
+
USE_AGNOSTIC_STRATEGIES,
|
|
23
|
+
commentedDirectives_VerificationReports,
|
|
24
|
+
// currentFileCommentedDirective,
|
|
25
|
+
// importedFileCommentedDirective,
|
|
26
|
+
commentedDirectiveMessage,
|
|
27
|
+
specificViolationMessage,
|
|
28
|
+
specificFailure,
|
|
29
|
+
} from "../constants/bases.js";
|
|
30
|
+
|
|
31
|
+
import { resolveImportPath } from "../../_commons/utilities/helpers.js";
|
|
32
|
+
import {
|
|
33
|
+
getCommentedDirectiveFromCurrentModule,
|
|
34
|
+
getVerifiedCommentedDirective,
|
|
35
|
+
getCommentedDirectiveFromImportedModule,
|
|
36
|
+
isImportBlocked,
|
|
37
|
+
makeMessageFromCommentedDirective,
|
|
38
|
+
findSpecificViolationMessage,
|
|
39
|
+
getStrategizedDirective,
|
|
40
|
+
} from "./helpers.js";
|
|
41
|
+
|
|
42
|
+
/* currentFileFlow */
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 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.
|
|
46
|
+
* @param {Readonly<import('@typescript-eslint/utils').TSESLint.RuleContext<typeof reExportNotSameMessageId | typeof importBreaksCommentedImportRulesMessageId | typeof noCommentedDirective | typeof commentedDirectiveVerificationFailed | typeof importNotStrategized | typeof exportNotStrategized, []>>} context The ESLint rule's `context` object.
|
|
47
|
+
* @returns {{skip: true; verifiedCommentedDirective: undefined;} | {skip: undefined; verifiedCommentedDirective: 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;}} Returns either an object with `skip: true` to disregard or one with the non-null `verifiedCommentedDirective`.
|
|
48
|
+
*/
|
|
49
|
+
export const currentFileFlow = (context) => {
|
|
50
|
+
// GETTING THE EXTENSION OF THE CURRENT FILE
|
|
51
|
+
const currentFileExtension = path.extname(context.filename);
|
|
52
|
+
|
|
53
|
+
// fails if the file is not JavaScript (TypeScript)
|
|
54
|
+
const iscurrentFileJS = EXTENSIONS.some(
|
|
55
|
+
(ext) => currentFileExtension === ext
|
|
56
|
+
);
|
|
57
|
+
if (!iscurrentFileJS) {
|
|
58
|
+
console.error(
|
|
59
|
+
"ERROR. Linted files for this rule should only be in JavaScript (TypeScript)."
|
|
60
|
+
);
|
|
61
|
+
return { skip: true };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// gets the commented directive from the current file
|
|
65
|
+
const commentedDirective = getCommentedDirectiveFromCurrentModule(context);
|
|
66
|
+
|
|
67
|
+
// reports if there is no directive or no valid directive (same, but eventually no directive could have defaults)
|
|
68
|
+
if (!commentedDirective) {
|
|
69
|
+
context.report({
|
|
70
|
+
loc: {
|
|
71
|
+
start: { line: 1, column: 0 },
|
|
72
|
+
end: { line: 1, column: context.sourceCode.lines[0].length },
|
|
73
|
+
},
|
|
74
|
+
messageId: noCommentedDirective,
|
|
75
|
+
});
|
|
76
|
+
return { skip: true };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const verifiedCommentedDirective = getVerifiedCommentedDirective(
|
|
80
|
+
commentedDirective,
|
|
81
|
+
currentFileExtension
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// reports if the verification failed
|
|
85
|
+
if (!verifiedCommentedDirective) {
|
|
86
|
+
context.report({
|
|
87
|
+
loc: {
|
|
88
|
+
start: { line: 1, column: 0 },
|
|
89
|
+
end: { line: 1, column: context.sourceCode.lines[0].length },
|
|
90
|
+
},
|
|
91
|
+
messageId: commentedDirectiveVerificationFailed,
|
|
92
|
+
data: {
|
|
93
|
+
[specificFailure]:
|
|
94
|
+
commentedDirectives_VerificationReports[commentedDirective],
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
return { skip: true };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
verifiedCommentedDirective,
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/* importedFileFlow */
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The flow that is shared between import and re-export traversals to obtain the import file's commented directive.
|
|
109
|
+
* @param {Readonly<import('@typescript-eslint/utils').TSESLint.RuleContext<typeof reExportNotSameMessageId | typeof importBreaksCommentedImportRulesMessageId | typeof noCommentedDirective | typeof commentedDirectiveVerificationFailed | typeof importNotStrategized | typeof exportNotStrategized, []>>} context The ESLint rule's `context` object.
|
|
110
|
+
* @param {import('@typescript-eslint/types').TSESTree.ImportDeclaration} node The ESLint `node` of the rule's current traversal.
|
|
111
|
+
* @returns {{skip: true; importedFileCommentedDirective: undefined;} | {skip: undefined; importedFileCommentedDirective: 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;}} Returns either an object with `skip: true` to disregard or one with the non-null `importedFileCommentedDirective`.
|
|
112
|
+
*/
|
|
113
|
+
const importedFileFlow = (context, node) => {
|
|
114
|
+
// finds the full path of the import
|
|
115
|
+
const resolvedImportPath = resolveImportPath(
|
|
116
|
+
path.dirname(context.filename),
|
|
117
|
+
node.source.value,
|
|
118
|
+
context.cwd
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// does not operate on paths it did not resolve
|
|
122
|
+
if (resolvedImportPath === null) return { skip: true };
|
|
123
|
+
// does not operate on non-JS files
|
|
124
|
+
const isImportedFileJS = EXTENSIONS.some((ext) =>
|
|
125
|
+
resolvedImportPath.endsWith(ext)
|
|
126
|
+
);
|
|
127
|
+
if (!isImportedFileJS) return { skip: true };
|
|
128
|
+
|
|
129
|
+
/* GETTING THE DIRECTIVE (or lack thereof) OF THE IMPORTED FILE */
|
|
130
|
+
let importedFileCommentedDirective =
|
|
131
|
+
getCommentedDirectiveFromImportedModule(resolvedImportPath);
|
|
132
|
+
|
|
133
|
+
// returns early if there is no directive or no valid directive (same, but eventually no directive could have defaults)
|
|
134
|
+
if (!importedFileCommentedDirective) {
|
|
135
|
+
console.warn(
|
|
136
|
+
"The imported file, whose path has been resolved, has no directive. It is thus ignored since the report on that circumstance is available on the imported file itself."
|
|
137
|
+
);
|
|
138
|
+
return { skip: true };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* GETTING THE CORRECT DIRECTIVE INTERPRETATION OF STRATEGY FOR AGNOSTIC STRATEGIES MODULES IMPORTS.
|
|
142
|
+
(The Directive-First Architecture does not check whether the export and import Strategies are the same at this time, meaning a @clientLogics strategy could be wrongly imported and interpreted as a @serverLogics strategy. However, Strategy exports are plan to be linting in the future within their own Agnostic Strategies Modules to ensure they respect import rules within their own scopes. It may also become possible to check whether the export and import Strategies are the same in the future when identifiers are defined and the same, especially for components modules where a convention could be to for all non-type export to be named and PascalCase.) */
|
|
143
|
+
if (importedFileCommentedDirective === USE_AGNOSTIC_STRATEGIES) {
|
|
144
|
+
importedFileCommentedDirective = getStrategizedDirective(context, node);
|
|
145
|
+
|
|
146
|
+
if (importedFileCommentedDirective === null) {
|
|
147
|
+
context.report({
|
|
148
|
+
node,
|
|
149
|
+
messageId: importNotStrategized,
|
|
150
|
+
});
|
|
151
|
+
return { skip: true };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { importedFileCommentedDirective };
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/* importsFlow */
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The full flow for import traversals to enforce effective directives import rules.
|
|
162
|
+
* @param {Readonly<import('@typescript-eslint/utils').TSESLint.RuleContext<typeof reExportNotSameMessageId | typeof importBreaksCommentedImportRulesMessageId | typeof noCommentedDirective | typeof commentedDirectiveVerificationFailed | typeof importNotStrategized | typeof exportNotStrategized, []>>} context The ESLint rule's `context` object.
|
|
163
|
+
* @param {import('@typescript-eslint/types').TSESTree.ImportDeclaration} node The ESLint `node` of the rule's current traversal.
|
|
164
|
+
* @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} currentFileCommentedDirective The current file's commented directive.
|
|
165
|
+
* @returns Returns early if the flow needs to be interrupted.
|
|
166
|
+
*/
|
|
167
|
+
export const importsFlow = (context, node, currentFileCommentedDirective) => {
|
|
168
|
+
// does not operate on `import type`
|
|
169
|
+
if (node.importKind === "type") return;
|
|
170
|
+
|
|
171
|
+
const result = importedFileFlow(context, node);
|
|
172
|
+
|
|
173
|
+
if (result.skip) return;
|
|
174
|
+
const { importedFileCommentedDirective } = result;
|
|
175
|
+
|
|
176
|
+
if (
|
|
177
|
+
isImportBlocked(
|
|
178
|
+
currentFileCommentedDirective,
|
|
179
|
+
importedFileCommentedDirective
|
|
180
|
+
)
|
|
181
|
+
) {
|
|
182
|
+
context.report({
|
|
183
|
+
node,
|
|
184
|
+
messageId: importBreaksCommentedImportRulesMessageId,
|
|
185
|
+
data: {
|
|
186
|
+
[commentedDirectiveMessage]: makeMessageFromCommentedDirective(
|
|
187
|
+
currentFileCommentedDirective
|
|
188
|
+
),
|
|
189
|
+
[specificViolationMessage]: findSpecificViolationMessage(
|
|
190
|
+
currentFileCommentedDirective,
|
|
191
|
+
importedFileCommentedDirective
|
|
192
|
+
),
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/* allExportsFlow */
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The full flow for export traversals, shared between `ExportNamedDeclaration`, `ExportAllDeclaration`, and `ExportDefaultDeclaration`, to ensure same commented directive re-exports in modules that aren't Agnostic Strategies Modules, and enforce strategized exports specifically in Agnostic Strategies Modules.
|
|
202
|
+
* @param {Readonly<import('@typescript-eslint/utils').TSESLint.RuleContext<typeof reExportNotSameMessageId | typeof importBreaksCommentedImportRulesMessageId | typeof noCommentedDirective | typeof commentedDirectiveVerificationFailed | typeof importNotStrategized | typeof exportNotStrategized, []>>} context The ESLint rule's `context` object.
|
|
203
|
+
* @param {import('@typescript-eslint/types').TSESTree.ExportNamedDeclaration | import('@typescript-eslint/types').TSESTree.ExportAllDeclaration | import('@typescript-eslint/types').TSESTree.ExportDefaultDeclaration} node The ESLint `node` of the rule's current traversal.
|
|
204
|
+
* @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} currentFileCommentedDirective The current file's commented directive.
|
|
205
|
+
* @returns Returns early if the flow needs to be interrupted.
|
|
206
|
+
*/
|
|
207
|
+
export const allExportsFlow = (
|
|
208
|
+
context,
|
|
209
|
+
node,
|
|
210
|
+
currentFileCommentedDirective
|
|
211
|
+
) => {
|
|
212
|
+
// does not operate on `export type`
|
|
213
|
+
if (node.exportKind === "type") return;
|
|
214
|
+
|
|
215
|
+
if (node.source) {
|
|
216
|
+
const result = importedFileFlow(context, node);
|
|
217
|
+
|
|
218
|
+
if (result.skip) return;
|
|
219
|
+
const { importedFileCommentedDirective } = result;
|
|
220
|
+
|
|
221
|
+
// ignores if this is NOT an Agnostic Strategies Module
|
|
222
|
+
// verifies current node export strategy if "use agnostic strategies"
|
|
223
|
+
if (currentFileCommentedDirective === USE_AGNOSTIC_STRATEGIES) {
|
|
224
|
+
const exportStrategizedDirective = getStrategizedDirective(context, node);
|
|
225
|
+
|
|
226
|
+
if (exportStrategizedDirective === null) {
|
|
227
|
+
context.report({
|
|
228
|
+
node,
|
|
229
|
+
messageId: exportNotStrategized,
|
|
230
|
+
});
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
currentFileCommentedDirective = exportStrategizedDirective;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (currentFileCommentedDirective !== importedFileCommentedDirective) {
|
|
238
|
+
context.report({
|
|
239
|
+
node,
|
|
240
|
+
messageId: reExportNotSameMessageId,
|
|
241
|
+
data: {
|
|
242
|
+
// currentFileCommentedDirective
|
|
243
|
+
currentFileCommentedDirective,
|
|
244
|
+
// importedFileCommentedDirective
|
|
245
|
+
importedFileCommentedDirective,
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
// ignores if this is NOT an Agnostic Strategies Module
|
|
252
|
+
// verifies current node export strategy if "use agnostic strategies"
|
|
253
|
+
if (currentFileCommentedDirective === USE_AGNOSTIC_STRATEGIES) {
|
|
254
|
+
const exportStrategizedDirective = getStrategizedDirective(context, node);
|
|
255
|
+
|
|
256
|
+
if (exportStrategizedDirective === null) {
|
|
257
|
+
context.report({
|
|
258
|
+
node,
|
|
259
|
+
messageId: exportNotStrategized,
|
|
260
|
+
});
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// just to emphasize that this is the same short flow from above
|
|
265
|
+
currentFileCommentedDirective = exportStrategizedDirective;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
};
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import {
|
|
2
|
+
reExportNotSameMessageId,
|
|
3
|
+
importBreaksCommentedImportRulesMessageId,
|
|
4
|
+
noCommentedDirective,
|
|
5
|
+
commentedDirectiveVerificationFailed,
|
|
6
|
+
importNotStrategized,
|
|
7
|
+
exportNotStrategized,
|
|
8
|
+
} from "../../_commons/constants/bases.js";
|
|
9
|
+
import {
|
|
10
|
+
USE_SERVER_LOGICS,
|
|
11
|
+
USE_CLIENT_LOGICS,
|
|
12
|
+
USE_AGNOSTIC_LOGICS,
|
|
13
|
+
USE_SERVER_COMPONENTS,
|
|
14
|
+
USE_CLIENT_COMPONENTS,
|
|
15
|
+
USE_AGNOSTIC_COMPONENTS,
|
|
16
|
+
USE_SERVER_FUNCTIONS,
|
|
17
|
+
USE_CLIENT_CONTEXTS,
|
|
18
|
+
USE_AGNOSTIC_CONDITIONS,
|
|
19
|
+
USE_AGNOSTIC_STRATEGIES,
|
|
20
|
+
directivesSet,
|
|
21
|
+
directivesArray,
|
|
22
|
+
commentedDirectives_4RawImplementations,
|
|
23
|
+
commentedStrategies_CommentedDirectives,
|
|
24
|
+
commentedDirectives_BlockedImports,
|
|
25
|
+
commentedDirectives_CommentedModules,
|
|
26
|
+
} from "../constants/bases.js";
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
getImportedFileFirstLine,
|
|
30
|
+
isImportBlocked as commonsIsImportBlocked,
|
|
31
|
+
makeMessageFromResolvedDirective,
|
|
32
|
+
findSpecificViolationMessage as commonsFindSpecificViolationMessage,
|
|
33
|
+
} from "../../_commons/utilities/helpers.js";
|
|
34
|
+
|
|
35
|
+
/* getCommentedDirectiveFromCurrentModule */
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Gets the commented directive of the current module.
|
|
39
|
+
*
|
|
40
|
+
* Accepted directives for the default Directive-First Architecture are (single or double quotes included):
|
|
41
|
+
* - `'use server logics'`, `"use server logics"` denoting a Server Logics Module.
|
|
42
|
+
* - `'use client logics'`, `"use client logics"` denoting a Client Logics Module.
|
|
43
|
+
* - `'use agnostic logics'`, `"use agnostic logics"` denoting an Agnostic Logics Module.
|
|
44
|
+
* - `'use server components'`, `"use server components"` denoting a Server Components Module.
|
|
45
|
+
* - `'use client components'`, `"use client components"` denoting a Client Components Module.
|
|
46
|
+
* - `'use agnostic components'`, `"use agnostic components"` denoting an Agnostic Components Module.
|
|
47
|
+
* - `'use agnostic logics'`, `"use agnostic logics"` denoting an Agnostic Logics Module.
|
|
48
|
+
* - `'use server functions'`, `"use server functions"` denoting a Server Functions Module.
|
|
49
|
+
* - `'use client contexts'`, `"use client contexts"` denoting a Client Contexts Module.
|
|
50
|
+
* - `'use agnostic conditions'`, `"use agnostic conditions"` denoting an Agnostic Conditions Module.
|
|
51
|
+
* - `'use agnostic strategies'`, `"use agnostic strategies"` denoting an Agnostic Strategies Module.
|
|
52
|
+
* @param {Readonly<import('@typescript-eslint/utils').TSESLint.RuleContext<typeof reExportNotSameMessageId | typeof importBreaksCommentedImportRulesMessageId | typeof noCommentedDirective | typeof commentedDirectiveVerificationFailed | typeof importNotStrategized | typeof exportNotStrategized, []>>} context The ESLint rule's `context` object.
|
|
53
|
+
* @returns {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 | null} The commented directive, or lack thereof via `null`. Given the strictness of this architecture, the lack of a directive is considered a mistake. (Though rules may provide the opportunity to declare a default, and configs with preset defaults may be provided.)
|
|
54
|
+
*/
|
|
55
|
+
export const getCommentedDirectiveFromCurrentModule = (context) => {
|
|
56
|
+
// gets the first comment from the source code
|
|
57
|
+
const firstComment = context.sourceCode.getAllComments()[0];
|
|
58
|
+
|
|
59
|
+
// returns null early if there is no first comment
|
|
60
|
+
if (!firstComment) return null;
|
|
61
|
+
|
|
62
|
+
// returns null early if the first comment is not on the first line and the first column
|
|
63
|
+
if (firstComment.loc.start.line !== 1 || firstComment.loc.start.column !== 0)
|
|
64
|
+
return null;
|
|
65
|
+
|
|
66
|
+
// gets the trimmed raw value of the first comment
|
|
67
|
+
const rawValue = firstComment.value.trim();
|
|
68
|
+
|
|
69
|
+
// checks if the raw value is single- or double-quoted (or neither)
|
|
70
|
+
const isSingleQuoted = detectQuoteType(rawValue);
|
|
71
|
+
|
|
72
|
+
// returns null early if the raw value (trimmed prior) is neither single- nor double-quoted
|
|
73
|
+
if (isSingleQuoted === null) return null;
|
|
74
|
+
|
|
75
|
+
// Obtains the value depending on whether the raw value is single- or double-quoted. (Note: The same string is returned if, for some impossible reason, the raw value does not correspond in terms of quote types. It does not matter because the check coming next will always fail to null if that's the case.)
|
|
76
|
+
const value = isSingleQuoted
|
|
77
|
+
? stripSingleQuotes(rawValue)
|
|
78
|
+
: stripDoubleQuotes(rawValue);
|
|
79
|
+
|
|
80
|
+
// certifies the directive or lack thereof from the obtained value
|
|
81
|
+
const commentedDirective = directivesSet.has(value) ? value : null;
|
|
82
|
+
|
|
83
|
+
return commentedDirective;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Detects whether a string is single- or double-quoted.
|
|
88
|
+
* @param {string} string
|
|
89
|
+
* @returns
|
|
90
|
+
*/
|
|
91
|
+
const detectQuoteType = (string) => {
|
|
92
|
+
if (string.startsWith("'") && string.endsWith("'")) {
|
|
93
|
+
return true; // single quotes
|
|
94
|
+
} else if (string.startsWith('"') && string.endsWith('"')) {
|
|
95
|
+
return false; // double quotes
|
|
96
|
+
} else {
|
|
97
|
+
return null; // neither
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Removes single quotes from a string known to be single-quoted.
|
|
103
|
+
* @param {string} string
|
|
104
|
+
* @returns
|
|
105
|
+
*/
|
|
106
|
+
const stripSingleQuotes = (string) => {
|
|
107
|
+
if (string.startsWith("'") && string.endsWith("'")) {
|
|
108
|
+
return string.slice(1, -1);
|
|
109
|
+
}
|
|
110
|
+
return string;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Removes double quotes from a string known to be double-quoted.
|
|
115
|
+
* @param {string} string
|
|
116
|
+
* @returns
|
|
117
|
+
*/
|
|
118
|
+
const stripDoubleQuotes = (string) => {
|
|
119
|
+
if (string.startsWith('"') && string.endsWith('"')) {
|
|
120
|
+
return string.slice(1, -1);
|
|
121
|
+
}
|
|
122
|
+
return string;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/* getVerifiedCommentedDirective */
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Ensures that a module's commented directive is consistent with its file extension (depending on whether it ends with 'x' for JSX).
|
|
129
|
+
* - `'use server logics'`: Server Logics Modules do NOT export JSX.
|
|
130
|
+
* - `'use client logics'`: Client Logics Modules do NOT export JSX.
|
|
131
|
+
* - `'use agnostic logics'`: Agnostic Logics Modules do NOT export JSX.
|
|
132
|
+
* - `'use server components'`: Server Components Modules ONLY export JSX.
|
|
133
|
+
* - `'use client components'`: Client Components Modules ONLY export JSX.
|
|
134
|
+
* - `'use agnostic components'`: Agnostic Components Modules ONLY export JSX.
|
|
135
|
+
* - `'use server functions'`: Server Functions Modules do NOT export JSX.
|
|
136
|
+
* - `'use client contexts'`: Client Contexts Modules ONLY export JSX.
|
|
137
|
+
* - `'use agnostic conditions'`: Agnostic Conditions Modules ONLY export JSX.
|
|
138
|
+
* - `'use agnostic strategies'`: Agnostic Strategies Modules may export JSX.
|
|
139
|
+
* @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} directive The commented directive as written on top of the file (cannot be `null` at that stage).
|
|
140
|
+
* @param {TSX | TS | JSX | JS | MJS | CJS} extension The JavaScript (TypeScript) extension of the file.
|
|
141
|
+
* @returns {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 | null} The verified commented directive, from which imports rules are applied. Returns `null` if the verification failed, upon which an error will be reported depending on the commented directive, since the error logic here is strictly binary.
|
|
142
|
+
*/
|
|
143
|
+
export const getVerifiedCommentedDirective = (directive, extension) => {
|
|
144
|
+
// I could use a map, but because this is in JS with JSDoc, a manual solution is peculiarly more typesafe.
|
|
145
|
+
if (directive === USE_SERVER_LOGICS && !extension.endsWith("x"))
|
|
146
|
+
return directive;
|
|
147
|
+
if (directive === USE_CLIENT_LOGICS && !extension.endsWith("x"))
|
|
148
|
+
return directive;
|
|
149
|
+
if (directive === USE_AGNOSTIC_LOGICS && !extension.endsWith("x"))
|
|
150
|
+
return directive;
|
|
151
|
+
if (directive === USE_SERVER_COMPONENTS && extension.endsWith("x"))
|
|
152
|
+
return directive;
|
|
153
|
+
if (directive === USE_CLIENT_COMPONENTS && extension.endsWith("x"))
|
|
154
|
+
return directive;
|
|
155
|
+
if (directive === USE_AGNOSTIC_COMPONENTS && extension.endsWith("x"))
|
|
156
|
+
return directive;
|
|
157
|
+
if (directive === USE_SERVER_FUNCTIONS && !extension.endsWith("x"))
|
|
158
|
+
return directive;
|
|
159
|
+
if (directive === USE_CLIENT_CONTEXTS && extension.endsWith("x"))
|
|
160
|
+
return directive;
|
|
161
|
+
if (directive === USE_AGNOSTIC_CONDITIONS && extension.endsWith("x"))
|
|
162
|
+
return directive;
|
|
163
|
+
if (directive === USE_AGNOSTIC_STRATEGIES) return directive;
|
|
164
|
+
|
|
165
|
+
return null; // verification error
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/* getCommentedDirectiveFromImportedModule */
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Gets the commented directive of the imported module.
|
|
172
|
+
*
|
|
173
|
+
* Accepted directives for the default Directive-First Architecture are (single or double quotes included):
|
|
174
|
+
* - `'use server logics'`, `"use server logics"` denoting a Server Logics Module.
|
|
175
|
+
* - `'use client logics'`, `"use client logics"` denoting a Client Logics Module.
|
|
176
|
+
* - `'use agnostic logics'`, `"use agnostic logics"` denoting an Agnostic Logics Module.
|
|
177
|
+
* - `'use server components'`, `"use server components"` denoting a Server Components Module.
|
|
178
|
+
* - `'use client components'`, `"use client components"` denoting a Client Components Module.
|
|
179
|
+
* - `'use agnostic components'`, `"use agnostic components"` denoting an Agnostic Components Module.
|
|
180
|
+
* - `'use agnostic logics'`, `"use agnostic logics"` denoting an Agnostic Logics Module.
|
|
181
|
+
* - `'use server functions'`, `"use server functions"` denoting a Server Functions Module.
|
|
182
|
+
* - `'use client contexts'`, `"use client contexts"` denoting a Client Contexts Module.
|
|
183
|
+
* - `'use agnostic conditions'`, `"use agnostic conditions"` denoting an Agnostic Conditions Module.
|
|
184
|
+
* - `'use agnostic strategies'`, `"use agnostic strategies"` denoting an Agnostic Strategies Module.
|
|
185
|
+
* @param {string} resolvedImportPath The resolved path of the import.
|
|
186
|
+
* @returns {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 | null} The commented directive, or lack thereof via `null`. Given the strictness of this architecture, the lack of a directive is considered a mistake. (Though rules may provide the opportunity to declare a default, and configs with preset defaults may be provided.)
|
|
187
|
+
*/
|
|
188
|
+
export const getCommentedDirectiveFromImportedModule = (resolvedImportPath) => {
|
|
189
|
+
// gets the first line of the code of the import
|
|
190
|
+
const importedFileFirstLine = getImportedFileFirstLine(resolvedImportPath);
|
|
191
|
+
|
|
192
|
+
// sees if the first line includes any of the directives and finds the directive that it includes
|
|
193
|
+
let includedDirective = "";
|
|
194
|
+
const lengthOne = directivesArray.length;
|
|
195
|
+
for (let i = 0; i < lengthOne; i++) {
|
|
196
|
+
const directive = directivesArray[i];
|
|
197
|
+
if (importedFileFirstLine.includes(directive)) {
|
|
198
|
+
includedDirective = directive;
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// returns null early if there is none of the directives in the first line
|
|
204
|
+
if (includedDirective === "") return null;
|
|
205
|
+
|
|
206
|
+
let importFileDirective = "";
|
|
207
|
+
const lengthTwo =
|
|
208
|
+
// sucks for that any but, I'm working in JS here
|
|
209
|
+
commentedDirectives_4RawImplementations[includedDirective].length;
|
|
210
|
+
for (let i = 0; i < lengthTwo; i++) {
|
|
211
|
+
const raw = commentedDirectives_4RawImplementations[includedDirective][i];
|
|
212
|
+
if (raw === importedFileFirstLine) {
|
|
213
|
+
importFileDirective = includedDirective;
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// returns null early if despite the presence of the directive it is not properly implemented
|
|
219
|
+
if (importFileDirective === "") return null;
|
|
220
|
+
|
|
221
|
+
return importFileDirective;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/* getStrategizedDirective */
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Gets the interpreted directive from a specified commented Strategy (such as `@serverLogics`) nested inside the import declaration for an import from an Agnostic Strategies Module.
|
|
228
|
+
* @param {Readonly<import('@typescript-eslint/utils').TSESLint.RuleContext<typeof reExportNotSameMessageId | typeof importBreaksCommentedImportRulesMessageId | typeof noCommentedDirective | typeof commentedDirectiveVerificationFailed | typeof importNotStrategized | typeof exportNotStrategized, []>>} context The ESLint rule's `context` object.
|
|
229
|
+
* @param {import('@typescript-eslint/types').TSESTree.ImportDeclaration} node The ESLint `node` of the rule's current traversal.
|
|
230
|
+
* @returns {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 | null} Returns the interpreted directive, a.k.a. strategized directive, or lack thereof via `null`.
|
|
231
|
+
*/
|
|
232
|
+
export const getStrategizedDirective = (context, node) => {
|
|
233
|
+
const firstNestedComment = context.sourceCode.getCommentsInside(node)[0];
|
|
234
|
+
|
|
235
|
+
// returns null early if there is no nested comments
|
|
236
|
+
if (!firstNestedComment) return null;
|
|
237
|
+
|
|
238
|
+
const strategy = firstNestedComment.value.trim() || null;
|
|
239
|
+
|
|
240
|
+
return commentedStrategies_CommentedDirectives[strategy] || null;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/* isImportBlocked */
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Returns a boolean deciding if an imported file's commented directive is incompatible with the current file's commented directive.
|
|
247
|
+
* @param {USE_SERVER_LOGICS | USE_SERVER_COMPONENTS | USE_SERVER_FUNCTIONS | USE_CLIENT_LOGICS | USE_CLIENT_COMPONENTS | USE_AGNOSTIC_LOGICS | USE_AGNOSTIC_COMPONENTS} currentFileCommentedDirective The current file's commented directive.
|
|
248
|
+
* @param {USE_SERVER_LOGICS | USE_SERVER_COMPONENTS | USE_SERVER_FUNCTIONS | USE_CLIENT_LOGICS | USE_CLIENT_COMPONENTS | USE_AGNOSTIC_LOGICS | USE_AGNOSTIC_COMPONENTS} importedFileCommentedDirective The imported file's commented directive.
|
|
249
|
+
* @returns {boolean} Returns `true` if the import is blocked, as established in `commentedDirectives_BlockedImports`.
|
|
250
|
+
*/
|
|
251
|
+
export const isImportBlocked = (
|
|
252
|
+
currentFileCommentedDirective,
|
|
253
|
+
importedFileCommentedDirective
|
|
254
|
+
) =>
|
|
255
|
+
commonsIsImportBlocked(
|
|
256
|
+
commentedDirectives_BlockedImports,
|
|
257
|
+
currentFileCommentedDirective,
|
|
258
|
+
importedFileCommentedDirective
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
/* makeMessageFromCommentedDirective */
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Lists in an message the commented modules incompatible with a commented module based on its commented directive.
|
|
265
|
+
* @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} commentedDirective The commented directive of the commented module.
|
|
266
|
+
* @returns {string} The message listing the incompatible commented modules.
|
|
267
|
+
*/
|
|
268
|
+
export const makeMessageFromCommentedDirective = (commentedDirective) =>
|
|
269
|
+
makeMessageFromResolvedDirective(
|
|
270
|
+
commentedDirectives_CommentedModules,
|
|
271
|
+
commentedDirectives_BlockedImports,
|
|
272
|
+
commentedDirective
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
/* findSpecificViolationMessage */
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Finds the `message` for the specific violation of commented directives import rules based on `commentedDirectives_BlockedImports`.
|
|
279
|
+
* @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} currentFileCommentedDirective The current file's commented directive.
|
|
280
|
+
* @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} importedFileCommentedDirective The imported file's commented directive.
|
|
281
|
+
* @returns {string} The corresponding `message`.
|
|
282
|
+
*/
|
|
283
|
+
export const findSpecificViolationMessage = (
|
|
284
|
+
currentFileCommentedDirective,
|
|
285
|
+
importedFileCommentedDirective
|
|
286
|
+
) =>
|
|
287
|
+
commonsFindSpecificViolationMessage(
|
|
288
|
+
commentedDirectives_BlockedImports,
|
|
289
|
+
currentFileCommentedDirective,
|
|
290
|
+
importedFileCommentedDirective
|
|
291
|
+
);
|
package/library/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
enforceEffectiveDirectivesRuleName,
|
|
5
|
+
enforceCommentedDirectivesRuleName,
|
|
6
|
+
} from "./_commons/constants/bases.js";
|
|
7
|
+
|
|
8
|
+
import enforceEffectiveDirectivesImportRules from "./agnostic20/rules/import-rules-enforcement.js";
|
|
9
|
+
import enforceCommentedDirectivesImportRules from "./directive21/rules/import-rules-enforcement.js";
|
|
10
|
+
|
|
11
|
+
import { makeAgnostic20Config } from "./agnostic20/config.js";
|
|
12
|
+
import { makeDirective21Config } from "./directive21/config.js";
|
|
13
|
+
|
|
14
|
+
/** @type {import("tsconfig-paths/lib/filesystem.js").PackageJson} */
|
|
15
|
+
const packageDotJSON = JSON.parse(
|
|
16
|
+
fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
/** @type {import('eslint').ESLint.Plugin} */
|
|
20
|
+
const plugin = {
|
|
21
|
+
meta: { ...packageDotJSON },
|
|
22
|
+
configs: {}, // applied below
|
|
23
|
+
rules: {
|
|
24
|
+
[enforceEffectiveDirectivesRuleName]: enforceEffectiveDirectivesImportRules,
|
|
25
|
+
[enforceCommentedDirectivesRuleName]: enforceCommentedDirectivesImportRules,
|
|
26
|
+
},
|
|
27
|
+
processors: {}, // not used
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
Object.assign(plugin.configs, makeAgnostic20Config(plugin));
|
|
31
|
+
Object.assign(plugin.configs, makeDirective21Config(plugin));
|
|
32
|
+
|
|
33
|
+
export default plugin;
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
// for use in eslint.config.js
|
|
37
|
+
useAgnosticPluginName,
|
|
38
|
+
agnostic20ConfigName,
|
|
39
|
+
directive21ConfigName,
|
|
40
|
+
enforceEffectiveDirectivesRuleName,
|
|
41
|
+
enforceCommentedDirectivesRuleName,
|
|
42
|
+
} from "./_commons/constants/bases.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eslint-plugin-use-agnostic",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "eslint-plugin-use-agnostic highlights problematic server-client imports in projects made with the Fullstack React Architecture (Next.js App Router, etc.) based on each of their modules' derived effective directives through detailed import rule violations, thanks to the introduction of its very own 'use agnostic' directive.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"eslint",
|
|
7
|
+
"eslintplugin",
|
|
8
|
+
"eslint-plugin",
|
|
9
|
+
"react",
|
|
10
|
+
"nextjs",
|
|
11
|
+
"next-js",
|
|
12
|
+
"remix",
|
|
13
|
+
"reactrouter",
|
|
14
|
+
"react-router"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "Luther Tchofo Safo <luther@tchofo-safo-portfolio.me>",
|
|
18
|
+
"files": [
|
|
19
|
+
"library"
|
|
20
|
+
],
|
|
21
|
+
"exports": {
|
|
22
|
+
".": "./library/index.js"
|
|
23
|
+
},
|
|
24
|
+
"main": "library/index.js",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/LutherTS/eslint-plugin-use-agnostic.git"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"tsconfig-paths": "^4.2.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@typescript-eslint/utils": "^8.31.1",
|
|
34
|
+
"eslint": ">=9.0.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"eslint": ">=9.0.0"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.18.0 <18.99 || >=20.9.0 <21.0.0 || >=21.1.0"
|
|
41
|
+
},
|
|
42
|
+
"type": "module"
|
|
43
|
+
}
|