eslint-plugin-pinia
Version:
ESLint plugin for Pinia best practices
458 lines (444 loc) • 14.7 kB
JavaScript
'use strict';
const utils = require('@typescript-eslint/utils');
const astUtils = require('@typescript-eslint/utils/ast-utils');
const path = require('path');
const createEslintRule = utils.ESLintUtils.RuleCreator(
(name) => `https://github.com/lisilinhart/eslint-plugin-pinia/blob/main/docs/rules/${name}.md`
);
function isRefOrReactiveCall(node) {
return !!node && node.type === utils.AST_NODE_TYPES.CallExpression && node.callee.type === "Identifier" && (node.callee.name === "ref" || node.callee.name === "reactive");
}
const RULE_NAME$6 = "require-setup-store-properties-export";
const requireSetupStorePropertiesExport = createEslintRule({
name: RULE_NAME$6,
meta: {
type: "problem",
docs: {
description: "In setup stores all state properties must be exported."
},
schema: [],
messages: {
missingVariables: "Missing state variable exports in return statement: {{variableNames}}"
}
},
defaultOptions: [],
create: (context) => {
return {
CallExpression(node) {
if (node.callee.type === utils.AST_NODE_TYPES.Identifier && node.callee.name === "defineStore" && node.arguments.length === 2 && node.arguments[1].type !== utils.AST_NODE_TYPES.ObjectExpression) {
const arrowFunc = node.arguments[1];
if (arrowFunc.body.type !== utils.AST_NODE_TYPES.BlockStatement)
return;
const declaredStateVariables = arrowFunc.body.body.filter(({ type }) => type === utils.AST_NODE_TYPES.VariableDeclaration).flatMap((declaration) => {
return declaration.declarations.filter(({ init, id }) => isRefOrReactiveCall(init) && astUtils.isIdentifier(id)).map(({ id }) => id.name);
});
if (declaredStateVariables.length <= 0)
return;
const returnStatement = arrowFunc.body.body.find(({ type }) => type === utils.AST_NODE_TYPES.ReturnStatement);
if (!returnStatement) {
return context.report({
node,
messageId: "missingVariables",
data: {
variableNames: declaredStateVariables.join(", ")
}
});
}
const returnedVariables = returnStatement?.argument?.type === utils.AST_NODE_TYPES.ObjectExpression ? returnStatement.argument.properties.flatMap(
(property) => property.type === utils.AST_NODE_TYPES.Property && property.value.type === utils.AST_NODE_TYPES.Identifier ? [property.value.name] : []
) : [];
const missingVariables = declaredStateVariables.filter(
(variable) => !returnedVariables.includes(variable)
);
if (missingVariables.length > 0) {
context.report({
node: returnStatement,
messageId: "missingVariables",
data: {
variableNames: missingVariables.join(", ")
}
});
}
}
}
};
}
});
const RULE_NAME$5 = "never-export-initialized-store";
const storeIds = /* @__PURE__ */ new Set();
const neverExportInitializedStore = createEslintRule({
name: RULE_NAME$5,
meta: {
type: "problem",
docs: {
description: "Never export an initialized named or default store."
},
schema: [],
messages: {
namedInitialization: "Never export an initialized store: {{storeName}}. Use inject/import instead where it is used.",
defaultInitialization: "Never export default initialized store. Use inject/import instead where it is used."
}
},
defaultOptions: [],
create: (context) => {
return {
CallExpression(node) {
if (node.callee.type === "Identifier" && node.callee.name === "defineStore" && node.arguments.length >= 2 && node.arguments[0].type === "Literal" && typeof node.arguments[0].value === "string" && node.parent.id.type === "Identifier") {
const callee = node.callee;
if (callee.type !== "Identifier" || callee.name !== "defineStore")
return;
const storeId = node.arguments && node.arguments[0];
if (!storeId || storeId.type !== utils.AST_NODE_TYPES.Literal)
return;
const value = node.parent.id.name;
storeIds.add(value);
}
},
ExportDefaultDeclaration(node) {
if (storeIds.has(node.declaration?.parent?.declaration?.callee?.name)) {
context.report({
node,
messageId: "defaultInitialization"
});
}
},
ExportNamedDeclaration(node) {
if (node?.declaration?.type === "VariableDeclaration") {
node?.declaration?.declarations.forEach((declaration) => {
if (storeIds.has(declaration?.init?.callee?.name)) {
context.report({
node,
messageId: "namedInitialization",
data: {
storeName: declaration?.init?.callee?.name
}
});
}
});
}
}
};
}
});
const RULE_NAME$4 = "prefer-use-store-naming-convention";
const preferUseStoreNamingConvention = createEslintRule({
name: RULE_NAME$4,
meta: {
type: "problem",
docs: {
description: "Enforces the convention of naming stores with the prefix `use` followed by the store name."
},
schema: [
{
type: "object",
properties: {
checkStoreNameMismatch: {
type: "boolean",
default: false
},
storeSuffix: {
type: "string",
default: ""
}
},
additionalProperties: false
}
],
messages: {
incorrectPrefix: 'Store names should start with "use" followed by the store name.',
incorrectSuffix: 'Store names should end with "{{ suffixName }}".',
storeNameMismatch: 'The "{{name}}" variable naming does not match the unique identifier "{{id}}" naming for the store.'
}
},
defaultOptions: [
{
checkStoreNameMismatch: false,
storeSuffix: ""
}
],
create: (context, options) => {
return {
CallExpression(node) {
if (node.callee.type === "Identifier" && node.callee.name === "defineStore" && node.arguments.length >= 2 && node.arguments[0].type === "Literal" && typeof node.arguments[0].value === "string" && node.parent.id.type === "Identifier") {
const { checkStoreNameMismatch, storeSuffix } = options[0];
const uniqueId = node.arguments[0].value;
const hasSuffixConfigured = storeSuffix.length > 0;
const expectedName = `use${uniqueId.charAt(0).toUpperCase()}${uniqueId.slice(1)}${storeSuffix}`;
const variableName = node.parent.id.name;
if (!variableName.startsWith("use")) {
context.report({
node: node.parent,
messageId: "incorrectPrefix"
});
}
if (hasSuffixConfigured && !variableName.endsWith(storeSuffix)) {
context.report({
node: node.parent,
messageId: "incorrectSuffix",
data: {
suffixName: storeSuffix
}
});
}
if (checkStoreNameMismatch && variableName !== expectedName) {
context.report({
node: node.arguments[0],
messageId: "storeNameMismatch",
data: {
name: variableName,
id: uniqueId
}
});
}
}
}
};
}
});
const RULE_NAME$3 = "prefer-single-store-per-file";
const preferSingleStorePerFile = createEslintRule({
name: RULE_NAME$3,
meta: {
type: "problem",
docs: {
description: "Encourages defining each store in a separate file."
},
schema: [],
messages: {
multipleStores: "Only one store definition per file is allowed."
}
},
defaultOptions: [],
create: (context) => {
let storeDeclaration = null;
return {
Program() {
storeDeclaration = null;
},
CallExpression(node) {
const callee = node.callee;
if (callee.type === "Identifier" && callee.name === "defineStore") {
if (!storeDeclaration) {
storeDeclaration = node;
} else {
context.report({
messageId: "multipleStores",
node: storeDeclaration
});
}
}
}
};
}
});
const RULE_NAME$2 = "no-return-global-properties";
const noReturnGlobalProperties = createEslintRule({
name: RULE_NAME$2,
meta: {
type: "problem",
docs: {
description: "Disallows returning globally provided properties from Pinia stores."
},
schema: [],
messages: {
returnGlobalProperties: "Do not return properties like {{property}} as they are globally available and should not be returned from stores."
}
},
defaultOptions: [],
create: (context) => {
const variablesUsingGlobalCallee = /* @__PURE__ */ new Set();
return {
VariableDeclaration(node) {
node.declarations.forEach((declaration) => {
if (declaration.init && declaration.init.type === "CallExpression") {
const calleeName = declaration.init.callee.name;
if (calleeName === "useRoute" || calleeName === "inject")
variablesUsingGlobalCallee.add(declaration.id.name);
}
});
},
ReturnStatement(node) {
const { argument } = node;
if (argument && argument.type === "ObjectExpression") {
const { properties } = argument;
if (!properties)
return;
properties.forEach((property) => {
if (variablesUsingGlobalCallee.has(property?.value?.name)) {
context.report({
messageId: "returnGlobalProperties",
node: property,
data: {
property: property?.value?.name
}
});
}
});
}
}
};
}
});
const RULE_NAME$1 = "no-duplicate-store-ids";
const storeIdsCache = /* @__PURE__ */ new Map();
const noDuplicateStoreIds = createEslintRule({
name: RULE_NAME$1,
meta: {
type: "problem",
docs: {
description: "Disallow duplicate store ids."
},
schema: [],
messages: {
duplicatedStoreIds: "No duplicated store ids allowed: {{storeId}}"
}
},
defaultOptions: [],
create: (context) => {
const filepath = path.resolve(context.physicalFilename ?? context.filename);
let crtStoreIds = storeIdsCache.get(filepath);
if (!crtStoreIds) {
crtStoreIds = /* @__PURE__ */ new Set();
storeIdsCache.set(filepath, crtStoreIds);
} else {
crtStoreIds.clear();
}
return {
CallExpression(node) {
const callee = node.callee;
const storeId = node.arguments?.[0];
if (callee.type !== utils.AST_NODE_TYPES.Identifier || callee.name !== "defineStore")
return;
if (!storeId || storeId.type !== utils.AST_NODE_TYPES.Literal)
return;
const value = storeId.value;
if (crtStoreIds.has(value)) {
reportError();
return;
} else {
crtStoreIds.add(value);
}
for (const [key, ids] of storeIdsCache) {
if (key !== filepath && ids.has(value)) {
reportError();
return;
}
}
function reportError() {
context.report({
node: storeId,
messageId: "duplicatedStoreIds",
data: {
storeId: value
}
});
}
}
};
}
});
const RULE_NAME = "no-store-to-refs-in-store";
const noStoreToRefsInStore = createEslintRule({
name: RULE_NAME,
meta: {
type: "problem",
docs: {
description: "Disallow use of storeToRefs inside defineStore"
},
schema: [],
messages: {
storeToRefs: "Do not use storeToRefs in other stores. Use the store as a whole directly."
}
},
defaultOptions: [],
create: (context) => {
return {
CallExpression(node) {
if (node.callee.type === "Identifier" && node.callee.name === "defineStore" && node.arguments.length >= 2) {
const functionBody = node.arguments[1];
if (functionBody.type === "ArrowFunctionExpression" || functionBody.type === "FunctionExpression") {
const body = functionBody.body;
if (body.type === "BlockStatement") {
body.body.forEach((statement) => {
if (statement.type === "VariableDeclaration" && statement.declarations.length > 0) {
statement.declarations.forEach((declaration) => {
if (declaration.init && declaration.init.type === "CallExpression" && declaration.init.callee.type === "Identifier" && declaration.init.callee.name === "storeToRefs") {
context.report({
node: declaration.init.callee,
messageId: "storeToRefs"
});
}
});
}
});
}
}
}
}
};
}
});
const rules = {
[RULE_NAME$5]: neverExportInitializedStore,
[RULE_NAME$1]: noDuplicateStoreIds,
[RULE_NAME$2]: noReturnGlobalProperties,
[RULE_NAME]: noStoreToRefsInStore,
[RULE_NAME$3]: preferSingleStorePerFile,
[RULE_NAME$4]: preferUseStoreNamingConvention,
[RULE_NAME$6]: requireSetupStorePropertiesExport
};
const plugin = {
rules
};
const allRules = {
[RULE_NAME$5]: "warn",
[RULE_NAME$1]: "warn",
[RULE_NAME$2]: "warn",
[RULE_NAME]: "warn",
[RULE_NAME$4]: "warn",
[RULE_NAME$3]: "off",
[RULE_NAME$6]: "warn"
};
const recommended = {
[RULE_NAME$5]: "error",
[RULE_NAME$1]: "error",
[RULE_NAME$2]: "error",
[RULE_NAME]: "error",
[RULE_NAME$4]: "warn",
[RULE_NAME$6]: "error"
};
function createConfig(_rules, flat = false) {
const name = "pinia";
const constructedRules = Object.keys(
_rules
).reduce((acc, ruleName) => {
return {
...acc,
[`${name}/${ruleName}`]: _rules[ruleName]
};
}, {});
if (flat) {
return {
plugins: {
[name]: plugin
},
rules: constructedRules
};
} else {
return {
plugins: [name],
rules: constructedRules
};
}
}
const configs = {
all: createConfig(allRules),
recommended: createConfig(recommended),
"all-flat": createConfig(allRules, true),
"recommended-flat": createConfig(recommended, true)
};
const index = {
...plugin,
configs
};
module.exports = index;