@next-friday/eslint-plugin-friday 1.0.0 → 1.0.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 +10 -10
- package/dist/index.mjs +104 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,16 +33,16 @@ The package exports rule implementations and their documented options.
|
|
|
33
33
|
|
|
34
34
|
🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).
|
|
35
35
|
|
|
36
|
-
| Name | Description
|
|
37
|
-
| :--------------------------------------------------------------------------------------- |
|
|
38
|
-
| [component-module](docs/rules/component-module.md) | require React component modules to contain only
|
|
39
|
-
| [index-export-only](docs/rules/index-export-only.md) | require index files to contain only imports, exports without inline runtime implementation, directives, and type declarations
|
|
40
|
-
| [jsx-newline-between-elements](docs/rules/jsx-newline-between-elements.md) | require empty lines between adjacent JSX elements, fragments, or expression containers when either is multi-line
|
|
41
|
-
| [jsx-no-newline-single-line-elements](docs/rules/jsx-no-newline-single-line-elements.md) | disallow empty lines between adjacent single-line JSX elements or fragments
|
|
42
|
-
| [named-props](docs/rules/named-props.md) | disallow inline intersections in the first parameter of React function components
|
|
43
|
-
| [no-lazy-identifiers](docs/rules/no-lazy-identifiers.md) | disallow lazy placeholder identifiers such as repeated characters and keyboard-row runs
|
|
44
|
-
| [object-curly-newline](docs/rules/object-curly-newline.md) | require every non-empty object literal to use multiline braces
|
|
45
|
-
| [props-in-body](docs/rules/props-in-body.md) | disallow object destructuring in the first parameter of React function components
|
|
36
|
+
| Name | Description | 🔧 |
|
|
37
|
+
| :--------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-- |
|
|
38
|
+
| [component-module](docs/rules/component-module.md) | require React component modules to contain only component definitions, component-support declarations, imports, exports, directives, and explicitly allowed framework declarations | |
|
|
39
|
+
| [index-export-only](docs/rules/index-export-only.md) | require index files to contain only imports, exports without inline runtime implementation, directives, and type declarations | |
|
|
40
|
+
| [jsx-newline-between-elements](docs/rules/jsx-newline-between-elements.md) | require empty lines between adjacent JSX elements, fragments, or expression containers when either is multi-line | 🔧 |
|
|
41
|
+
| [jsx-no-newline-single-line-elements](docs/rules/jsx-no-newline-single-line-elements.md) | disallow empty lines between adjacent single-line JSX elements or fragments | 🔧 |
|
|
42
|
+
| [named-props](docs/rules/named-props.md) | disallow inline intersections in the first parameter of React function components | |
|
|
43
|
+
| [no-lazy-identifiers](docs/rules/no-lazy-identifiers.md) | disallow lazy placeholder identifiers such as repeated characters and keyboard-row runs | |
|
|
44
|
+
| [object-curly-newline](docs/rules/object-curly-newline.md) | require every non-empty object literal to use multiline braces | 🔧 |
|
|
45
|
+
| [props-in-body](docs/rules/props-in-body.md) | disallow object destructuring in the first parameter of React function components | |
|
|
46
46
|
|
|
47
47
|
<!-- end auto-generated rules list -->
|
|
48
48
|
|
package/dist/index.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
const meta = {
|
|
6
6
|
name: "@next-friday/eslint-plugin-friday",
|
|
7
7
|
namespace: "friday",
|
|
8
|
-
version: "1.0.
|
|
8
|
+
version: "1.0.1"
|
|
9
9
|
};
|
|
10
10
|
//#endregion
|
|
11
11
|
//#region src/utils/function-component-visitor.ts
|
|
@@ -37,8 +37,10 @@ const MESSAGE_ID$6 = "componentModule";
|
|
|
37
37
|
const ALLOWED_MODULE_STATEMENT_TYPES = /* @__PURE__ */ new Set([
|
|
38
38
|
"ExportAllDeclaration",
|
|
39
39
|
"ImportDeclaration",
|
|
40
|
-
"TSInterfaceDeclaration"
|
|
40
|
+
"TSInterfaceDeclaration",
|
|
41
|
+
"TSTypeAliasDeclaration"
|
|
41
42
|
]);
|
|
43
|
+
const COMPONENT_METADATA_PROPERTIES = /* @__PURE__ */ new Set(["displayName"]);
|
|
42
44
|
const EXPORT_STATEMENT_TYPES = /* @__PURE__ */ new Set(["ExportDefaultDeclaration", "ExportNamedDeclaration"]);
|
|
43
45
|
/**
|
|
44
46
|
* Check whether a declaration contains only React function components.
|
|
@@ -54,6 +56,89 @@ function isAllowedComponentDeclaration(node, components) {
|
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
/**
|
|
59
|
+
* Collect local React bindings needed to recognize component-support declarations.
|
|
60
|
+
* @param statements Top-level program statements.
|
|
61
|
+
* @returns Local createContext and React namespace/default import names.
|
|
62
|
+
*/
|
|
63
|
+
function getReactBindings(statements) {
|
|
64
|
+
const createContextNames = /* @__PURE__ */ new Set();
|
|
65
|
+
const namespaceNames = /* @__PURE__ */ new Set();
|
|
66
|
+
for (const statement of statements) {
|
|
67
|
+
if (statement.type !== "ImportDeclaration" || statement.importKind === "type" || statement.source?.value !== "react") continue;
|
|
68
|
+
const specifiers = statement.specifiers;
|
|
69
|
+
for (const specifier of specifiers) {
|
|
70
|
+
if (specifier.importKind === "type") continue;
|
|
71
|
+
if (specifier.type === "ImportSpecifier" && specifier.imported?.name === "createContext" && specifier.local?.name !== void 0) createContextNames.add(specifier.local.name);
|
|
72
|
+
if ((specifier.type === "ImportDefaultSpecifier" || specifier.type === "ImportNamespaceSpecifier") && specifier.local?.name !== void 0) namespaceNames.add(specifier.local.name);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
createContextNames,
|
|
77
|
+
namespaceNames
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Check whether a declaration is owned directly by the program body or an export wrapper.
|
|
82
|
+
* @param declaration Declaration node to locate.
|
|
83
|
+
* @param statements Top-level program statements.
|
|
84
|
+
* @returns Whether the declaration belongs to module scope.
|
|
85
|
+
*/
|
|
86
|
+
function isTopLevelDeclaration(declaration, statements) {
|
|
87
|
+
return statements.some((statement) => Object.is(statement, declaration) || EXPORT_STATEMENT_TYPES.has(statement.type) && Object.is(statement.declaration, declaration));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Collect module-scope bindings for detected React function components.
|
|
91
|
+
* @param components React function components detected by the React detector.
|
|
92
|
+
* @param statements Top-level program statements.
|
|
93
|
+
* @returns Component bindings that can own component metadata or identifier exports.
|
|
94
|
+
*/
|
|
95
|
+
function getComponentNames(components, statements) {
|
|
96
|
+
const names = /* @__PURE__ */ new Set();
|
|
97
|
+
for (const component of components) {
|
|
98
|
+
const initPath = component.initPath;
|
|
99
|
+
if (initPath === null || !isTopLevelDeclaration(initPath[0], statements)) continue;
|
|
100
|
+
if (initPath.length === 1) {
|
|
101
|
+
const declaration = initPath[0];
|
|
102
|
+
if (declaration.id?.name !== void 0) names.add(declaration.id.name);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const declarator = initPath[1];
|
|
106
|
+
names.add(declarator.id.name);
|
|
107
|
+
}
|
|
108
|
+
return names;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Check whether an expression calls React.createContext through a verified React import.
|
|
112
|
+
* @param expression Candidate initializer.
|
|
113
|
+
* @param reactBindings Local React bindings.
|
|
114
|
+
* @returns Whether the expression is a React createContext call.
|
|
115
|
+
*/
|
|
116
|
+
function isReactCreateContextCall(expression, reactBindings) {
|
|
117
|
+
if (expression?.type !== "CallExpression") return false;
|
|
118
|
+
const { callee } = expression;
|
|
119
|
+
return callee?.type === "Identifier" && callee.name !== void 0 && reactBindings.createContextNames.has(callee.name) ? true : callee?.type === "MemberExpression" && callee.computed !== true && callee.object?.type === "Identifier" && callee.object.name !== void 0 && reactBindings.namespaceNames.has(callee.object.name) && callee.property?.type === "Identifier" && callee.property.name === "createContext";
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Check whether a variable declaration contains only React structural declarations.
|
|
123
|
+
* @param node Declaration node to evaluate.
|
|
124
|
+
* @param reactBindings Local React bindings.
|
|
125
|
+
* @returns Whether every declared value is a React context.
|
|
126
|
+
*/
|
|
127
|
+
function isAllowedReactStructuralDeclaration(node, reactBindings) {
|
|
128
|
+
return node.type === "VariableDeclaration" && node.declarations !== void 0 && node.declarations.length > 0 && node.declarations.every((declaration) => isReactCreateContextCall(declaration.init, reactBindings));
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Check whether a top-level statement assigns supported metadata to a detected component.
|
|
132
|
+
* @param node Top-level program statement.
|
|
133
|
+
* @param componentNames Detected local React component names.
|
|
134
|
+
* @returns Whether the statement is supported component metadata.
|
|
135
|
+
*/
|
|
136
|
+
function isAllowedComponentMetadataStatement(node, componentNames) {
|
|
137
|
+
if (node.type !== "ExpressionStatement" || node.expression?.type !== "AssignmentExpression" || node.expression.operator !== "=") return false;
|
|
138
|
+
const { left, right } = node.expression;
|
|
139
|
+
return left.type === "MemberExpression" && left.computed !== true && left.object?.type === "Identifier" && left.object.name !== void 0 && componentNames.has(left.object.name) && left.property?.type === "Identifier" && left.property.name !== void 0 && COMPONENT_METADATA_PROPERTIES.has(left.property.name) && right.type === "Literal" && typeof right.value === "string";
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
57
142
|
* Check whether a declaration name is explicitly allowed by framework policy.
|
|
58
143
|
* @param node Declaration node to evaluate.
|
|
59
144
|
* @param allowedDeclarations Framework-owned declaration names.
|
|
@@ -70,19 +155,21 @@ function isAllowedDeclarationName(node, allowedDeclarations) {
|
|
|
70
155
|
* Check whether an export contains only allowed declarations.
|
|
71
156
|
* @param node Export statement to evaluate.
|
|
72
157
|
* @param components React function components detected by the React detector.
|
|
158
|
+
* @param componentNames Detected local React component names.
|
|
159
|
+
* @param reactBindings Local React bindings.
|
|
73
160
|
* @param allowedDeclarations Framework-owned declaration names.
|
|
74
161
|
* @returns Whether the export belongs in the component module.
|
|
75
162
|
*/
|
|
76
|
-
function isAllowedExportStatement(node, components, allowedDeclarations) {
|
|
163
|
+
function isAllowedExportStatement(node, components, componentNames, reactBindings, allowedDeclarations) {
|
|
77
164
|
if (!EXPORT_STATEMENT_TYPES.has(node.type)) return false;
|
|
78
165
|
if (!node.declaration) return true;
|
|
79
166
|
const declaration = node.declaration;
|
|
80
|
-
return declaration.type === "TSInterfaceDeclaration" || isAllowedComponentDeclaration(declaration, components) || isAllowedDeclarationName(declaration, allowedDeclarations);
|
|
167
|
+
return declaration.type === "TSInterfaceDeclaration" || declaration.type === "TSTypeAliasDeclaration" || declaration.type === "Identifier" && declaration.name !== void 0 && componentNames.has(declaration.name) || isAllowedComponentDeclaration(declaration, components) || isAllowedReactStructuralDeclaration(declaration, reactBindings) || isAllowedDeclarationName(declaration, allowedDeclarations);
|
|
81
168
|
}
|
|
82
169
|
/**
|
|
83
170
|
* Check whether the statement is permitted module syntax.
|
|
84
171
|
* @param node Top-level program statement.
|
|
85
|
-
* @returns Whether the statement is an import, re-export,
|
|
172
|
+
* @returns Whether the statement is an import, re-export, type declaration, or directive.
|
|
86
173
|
*/
|
|
87
174
|
function isAllowedModuleSyntax(node) {
|
|
88
175
|
return ALLOWED_MODULE_STATEMENT_TYPES.has(node.type) || node.type === "ExpressionStatement" && node.directive !== void 0;
|
|
@@ -91,33 +178,33 @@ function isAllowedModuleSyntax(node) {
|
|
|
91
178
|
* Check whether a top-level statement is permitted in a React component module.
|
|
92
179
|
* @param node Top-level program statement.
|
|
93
180
|
* @param components React function components detected by the React detector.
|
|
181
|
+
* @param componentNames Detected local React component names.
|
|
182
|
+
* @param reactBindings Local React bindings.
|
|
94
183
|
* @param allowedDeclarations Framework-owned declaration names.
|
|
95
184
|
* @returns Whether the statement belongs in a JSX or TSX component module.
|
|
96
185
|
*/
|
|
97
|
-
function isAllowedStatement$1(node, components, allowedDeclarations) {
|
|
98
|
-
return isAllowedModuleSyntax(node) || isAllowedComponentDeclaration(node, components) || isAllowedExportStatement(node, components, allowedDeclarations);
|
|
186
|
+
function isAllowedStatement$1(node, components, componentNames, reactBindings, allowedDeclarations) {
|
|
187
|
+
return isAllowedModuleSyntax(node) || isAllowedComponentDeclaration(node, components) || isAllowedReactStructuralDeclaration(node, reactBindings) || isAllowedComponentMetadataStatement(node, componentNames) || isAllowedExportStatement(node, components, componentNames, reactBindings, allowedDeclarations);
|
|
99
188
|
}
|
|
100
189
|
const componentModule = {
|
|
101
190
|
create(context) {
|
|
102
191
|
const [options] = context.options;
|
|
103
192
|
const allowedDeclarations = new Set(options?.allowDeclarations);
|
|
104
193
|
return createFunctionComponentVisitor(context, (node, components) => {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
});
|
|
113
|
-
}
|
|
194
|
+
const statements = node.body;
|
|
195
|
+
const componentNames = getComponentNames(components, statements);
|
|
196
|
+
const reactBindings = getReactBindings(statements);
|
|
197
|
+
for (const statement of statements) if (!isAllowedStatement$1(statement, components, componentNames, reactBindings, allowedDeclarations)) context.report({
|
|
198
|
+
messageId: MESSAGE_ID$6,
|
|
199
|
+
node: statement
|
|
200
|
+
});
|
|
114
201
|
});
|
|
115
202
|
},
|
|
116
203
|
meta: {
|
|
117
204
|
languages: ["js/js"],
|
|
118
205
|
type: "suggestion",
|
|
119
206
|
docs: {
|
|
120
|
-
description: "require React component modules to contain only
|
|
207
|
+
description: "require React component modules to contain only component definitions, component-support declarations, imports, exports, directives, and explicitly allowed framework declarations",
|
|
121
208
|
url: getRuleDocumentationUrl("component-module")
|
|
122
209
|
},
|
|
123
210
|
defaultOptions: [{ allowDeclarations: [] }],
|