| |
| var reactComponentNameRE = /^[A-Z][a-zA-Z0-9_]*$/u; |
| var onlyExportComponents = { |
| meta: { |
| messages: { |
| exportAll: "This rule can't verify that `export *` only exports components.", |
| namedExport: "Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.", |
| anonymousExport: "Fast refresh can't handle anonymous components. Add a name to your export.", |
| localComponents: "Fast refresh only works when a file only exports components. Move your component(s) to a separate file. If all exports are HOCs, add them to the `extraHOCs` option.", |
| noExport: "Fast refresh only works when a file has exports. Move your component(s) to a separate file.", |
| reactContext: "Fast refresh only works when a file only exports components. Move your React context(s) to a separate file." |
| }, |
| type: "problem", |
| schema: [ |
| { |
| type: "object", |
| properties: { |
| extraHOCs: { type: "array", items: { type: "string" } }, |
| allowExportNames: { type: "array", items: { type: "string" } }, |
| allowConstantExport: { type: "boolean" }, |
| checkJS: { type: "boolean" } |
| }, |
| additionalProperties: false |
| } |
| ] |
| }, |
| defaultOptions: [], |
| create: (context) => { |
| const { |
| extraHOCs = [], |
| allowExportNames, |
| allowConstantExport = false, |
| checkJS = false |
| } = context.options[0] ?? {}; |
| const filename = context.filename; |
| if (filename.includes(".test.") || filename.includes(".spec.") || filename.includes(".cy.") || filename.includes(".stories.")) { |
| return {}; |
| } |
| const shouldScan = filename.endsWith(".jsx") || filename.endsWith(".tsx") || checkJS && filename.endsWith(".js"); |
| if (!shouldScan) return {}; |
| const allowExportNamesSet = allowExportNames ? new Set(allowExportNames) : void 0; |
| const validHOCs = ["memo", "forwardRef", "lazy", ...extraHOCs]; |
| const getHocName = (node) => { |
| const callee = node.type === "CallExpression" ? node.callee : node.tag; |
| if (callee.type === "CallExpression") { |
| return getHocName(callee); |
| } |
| if (callee.type === "MemberExpression") { |
| if (callee.property.type === "Identifier" && validHOCs.includes(callee.property.name)) { |
| return callee.property.name; |
| } |
| if (callee.object.type === "Identifier" && validHOCs.includes(callee.object.name)) { |
| return callee.object.name; |
| } |
| if (callee.object.type === "CallExpression") { |
| return getHocName(callee.object); |
| } |
| } |
| if (callee.type === "Identifier") { |
| return callee.name; |
| } |
| return void 0; |
| }; |
| const isCallExpressionReactComponent = (node) => { |
| const hocName = getHocName(node); |
| if (!hocName || !validHOCs.includes(hocName)) return false; |
| const validateArgument = hocName === "memo" || hocName === "forwardRef"; |
| if (!validateArgument) return true; |
| if (node.arguments.length === 0) return false; |
| const arg = skipTSWrapper(node.arguments[0]); |
| switch (arg.type) { |
| case "Identifier": |
| return reactComponentNameRE.test(arg.name); |
| case "FunctionExpression": |
| case "ArrowFunctionExpression": |
| if (!arg.id) return "needName"; |
| return reactComponentNameRE.test(arg.id.name); |
| case "CallExpression": |
| return isCallExpressionReactComponent(arg); |
| default: |
| return false; |
| } |
| }; |
| const isExpressionReactComponent = (expressionParam) => { |
| const exp = skipTSWrapper(expressionParam); |
| if (exp.type === "Identifier") { |
| return reactComponentNameRE.test(exp.name); |
| } |
| if (exp.type === "ArrowFunctionExpression" || exp.type === "FunctionExpression") { |
| if (exp.params.length > 2) return false; |
| if (!exp.id?.name) return "needName"; |
| return reactComponentNameRE.test(exp.id.name); |
| } |
| if (exp.type === "ConditionalExpression") { |
| const consequent = isExpressionReactComponent(exp.consequent); |
| const alternate = isExpressionReactComponent(exp.alternate); |
| if (consequent === false || alternate === false) return false; |
| if (consequent === "needName" || alternate === "needName") { |
| return "needName"; |
| } |
| return true; |
| } |
| if (exp.type === "CallExpression") { |
| return isCallExpressionReactComponent(exp); |
| } |
| if (exp.type === "TaggedTemplateExpression") { |
| const hocName = getHocName(exp); |
| if (!hocName || !validHOCs.includes(hocName)) return false; |
| return "needName"; |
| } |
| return false; |
| }; |
| return { |
| Program(program) { |
| let hasExports = false; |
| let hasReactExport = false; |
| let reactIsInScope = false; |
| const localComponents = []; |
| const nonComponentExports = []; |
| const reactContextExports = []; |
| const handleExportIdentifier = (identifierNode, initParam) => { |
| if (identifierNode.type !== "Identifier") { |
| nonComponentExports.push(identifierNode); |
| return; |
| } |
| if (allowExportNamesSet?.has(identifierNode.name)) return; |
| if (!initParam) { |
| if (reactComponentNameRE.test(identifierNode.name)) { |
| hasReactExport = true; |
| } else { |
| nonComponentExports.push(identifierNode); |
| } |
| return; |
| } |
| const init = skipTSWrapper(initParam); |
| if (allowConstantExport && constantExportExpressions.has(init.type)) { |
| return; |
| } |
| if (init.type === "CallExpression" && (init.callee.type === "Identifier" && init.callee.name === "createContext" || init.callee.type === "MemberExpression" && init.callee.property.type === "Identifier" && init.callee.property.name === "createContext")) { |
| reactContextExports.push(identifierNode); |
| return; |
| } |
| const isReactComponent = reactComponentNameRE.test(identifierNode.name) && isExpressionReactComponent(init); |
| if (isReactComponent === false) { |
| nonComponentExports.push(identifierNode); |
| } else { |
| hasReactExport = true; |
| } |
| }; |
| const handleExportDeclaration = (node) => { |
| if (node.type === "VariableDeclaration") { |
| for (const variable of node.declarations) { |
| if (variable.init === null) { |
| nonComponentExports.push(variable.id); |
| continue; |
| } |
| handleExportIdentifier(variable.id, variable.init); |
| } |
| } else if (node.type === "FunctionDeclaration") { |
| if (node.id === null) { |
| context.report({ messageId: "anonymousExport", node }); |
| } else { |
| handleExportIdentifier(node.id); |
| } |
| } else if (node.type === "ClassDeclaration") { |
| if (node.id === null) { |
| context.report({ messageId: "anonymousExport", node }); |
| } else if (reactComponentNameRE.test(node.id.name) && node.superClass !== null && node.body.body.some( |
| (item) => item.type === "MethodDefinition" && item.key.type === "Identifier" && item.key.name === "render" |
| )) { |
| hasReactExport = true; |
| } else { |
| nonComponentExports.push(node.id); |
| } |
| } else if (node.type === "CallExpression") { |
| const result = isCallExpressionReactComponent(node); |
| if (result === false) { |
| nonComponentExports.push(node); |
| } else if (result === "needName") { |
| context.report({ messageId: "anonymousExport", node }); |
| } else { |
| hasReactExport = true; |
| } |
| } else { |
| nonComponentExports.push(node); |
| } |
| }; |
| for (const node of program.body) { |
| if (node.type === "ExportAllDeclaration") { |
| if (node.exportKind === "type") continue; |
| hasExports = true; |
| context.report({ messageId: "exportAll", node }); |
| } else if (node.type === "ExportDefaultDeclaration") { |
| hasExports = true; |
| const declaration = skipTSWrapper(node.declaration); |
| if (declaration.type === "VariableDeclaration" || declaration.type === "FunctionDeclaration" || declaration.type === "ClassDeclaration" || declaration.type === "CallExpression") { |
| handleExportDeclaration(declaration); |
| } |
| if (declaration.type === "Identifier") { |
| handleExportIdentifier(declaration); |
| } |
| if (declaration.type === "ArrowFunctionExpression") { |
| context.report({ messageId: "anonymousExport", node }); |
| } |
| } else if (node.type === "ExportNamedDeclaration") { |
| if (node.exportKind === "type") continue; |
| const declaration = node.declaration ? skipTSWrapper(node.declaration) : null; |
| if (declaration?.type === "TSDeclareFunction") continue; |
| hasExports = true; |
| if (declaration) handleExportDeclaration(declaration); |
| for (const specifier of node.specifiers) { |
| handleExportIdentifier( |
| specifier.exported.type === "Identifier" && specifier.exported.name === "default" ? specifier.local : specifier.exported |
| ); |
| } |
| } else if (node.type === "VariableDeclaration") { |
| for (const variable of node.declarations) { |
| if (variable.id.type === "Identifier" && reactComponentNameRE.test(variable.id.name) && variable.init !== null && isExpressionReactComponent(variable.init) !== false) { |
| localComponents.push(variable.id); |
| } |
| } |
| } else if (node.type === "FunctionDeclaration") { |
| if (reactComponentNameRE.test(node.id.name)) { |
| localComponents.push(node.id); |
| } |
| } else if (node.type === "ImportDeclaration" && node.source.value === "react") { |
| reactIsInScope = true; |
| } |
| } |
| if (checkJS && !reactIsInScope) return; |
| if (hasExports) { |
| if (hasReactExport) { |
| for (const node of nonComponentExports) { |
| context.report({ messageId: "namedExport", node }); |
| } |
| for (const node of reactContextExports) { |
| context.report({ messageId: "reactContext", node }); |
| } |
| } else if (localComponents.length) { |
| for (const node of localComponents) { |
| context.report({ messageId: "localComponents", node }); |
| } |
| } |
| } else if (localComponents.length) { |
| for (const node of localComponents) { |
| context.report({ messageId: "noExport", node }); |
| } |
| } |
| } |
| }; |
| } |
| }; |
| var skipTSWrapper = (node) => { |
| if (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" || node.type === "TSNonNullExpression" || node.type === "TSTypeAssertion" || node.type === "TSInstantiationExpression") { |
| return node.expression; |
| } |
| return node; |
| }; |
| var constantExportExpressions = new Set([ |
| "Literal", |
| |
| "UnaryExpression", |
| |
| "TemplateLiteral", |
| |
| "BinaryExpression" |
| |
| ]); |
|
|
| |
| var rules = { |
| "only-export-components": onlyExportComponents |
| }; |
| var plugin = { rules }; |
| var buildConfig = ({ |
| name, |
| baseOptions |
| }) => (options) => ({ |
| name: `react-refresh/${name}`, |
| plugins: { "react-refresh": plugin }, |
| rules: { |
| "react-refresh/only-export-components": [ |
| "error", |
| { ...baseOptions, ...options } |
| ] |
| } |
| }); |
| var configs = { |
| recommended: buildConfig({ name: "recommended", baseOptions: {} }), |
| vite: buildConfig({ |
| name: "vite", |
| baseOptions: { allowConstantExport: true } |
| }), |
| next: buildConfig({ |
| name: "next", |
| baseOptions: { |
| allowExportNames: [ |
| |
| "experimental_ppr", |
| "dynamic", |
| "dynamicParams", |
| "revalidate", |
| "fetchCache", |
| "runtime", |
| "preferredRegion", |
| "maxDuration", |
| |
| "metadata", |
| "generateMetadata", |
| |
| "viewport", |
| "generateViewport", |
| |
| "generateImageMetadata", |
| |
| "generateSitemaps", |
| |
| "generateStaticParams" |
| ] |
| } |
| }) |
| }; |
| var reactRefresh = { plugin, configs }; |
| var index_default = { |
| rules, |
| configs: { |
| recommended: configs.recommended(), |
| vite: configs.vite(), |
| next: configs.next() |
| } |
| }; |
| export { |
| index_default as default, |
| reactRefresh |
| }; |
|
|