mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
Catch Next.js invalid-use-server-value at lint time and move shared form-error copy out of plazaStaffFormActions so the use server module only exports async actions.
291 lines
8.4 KiB
JavaScript
291 lines
8.4 KiB
JavaScript
const isUppercaseName = (node) =>
|
|
node.type === 'Identifier' && /^[A-Z][A-Za-z0-9]*$/.test(node.name)
|
|
|
|
const isJsx = (node) => node?.type === 'JSXElement' || node?.type === 'JSXFragment'
|
|
|
|
const noComponentDefaultExport = {
|
|
meta: {
|
|
type: 'suggestion',
|
|
docs: {
|
|
description: 'Require ordinary React components to use named exports',
|
|
},
|
|
schema: [],
|
|
messages: {
|
|
defaultExport: 'React component files must not use default exports.',
|
|
},
|
|
},
|
|
create(context) {
|
|
return {
|
|
ExportDefaultDeclaration(node) {
|
|
context.report({ node, messageId: 'defaultExport' })
|
|
},
|
|
}
|
|
},
|
|
}
|
|
|
|
const frameworkDefaultExport = {
|
|
meta: {
|
|
type: 'problem',
|
|
docs: {
|
|
description: 'Require Next framework components to use inline named function defaults',
|
|
},
|
|
schema: [],
|
|
messages: {
|
|
invalidDefault:
|
|
'Next framework components must default-export an inline named function declaration.',
|
|
missingDefault: 'Next framework components must have a default export.',
|
|
},
|
|
},
|
|
create(context) {
|
|
let hasDefaultExport = false
|
|
|
|
return {
|
|
ExportDefaultDeclaration(node) {
|
|
hasDefaultExport = true
|
|
const declaration = node.declaration
|
|
const isNamedFunction =
|
|
declaration.type === 'FunctionDeclaration' && declaration.id !== null
|
|
|
|
if (!isNamedFunction) {
|
|
context.report({ node, messageId: 'invalidDefault' })
|
|
}
|
|
},
|
|
'Program:exit'(node) {
|
|
if (!hasDefaultExport) {
|
|
context.report({ node, messageId: 'missingDefault' })
|
|
}
|
|
},
|
|
}
|
|
},
|
|
}
|
|
|
|
const componentArrowConventions = {
|
|
meta: {
|
|
type: 'suggestion',
|
|
docs: {
|
|
description: 'Require const component arrows and implicit returns for pure JSX bodies',
|
|
},
|
|
fixable: 'code',
|
|
schema: [],
|
|
messages: {
|
|
useConst: 'Uppercase component arrow declarations must use const.',
|
|
implicitReturn: 'Pure JSX component arrow bodies must use an implicit return.',
|
|
},
|
|
},
|
|
create(context) {
|
|
const sourceCode = context.sourceCode
|
|
|
|
return {
|
|
VariableDeclarator(node) {
|
|
if (!isUppercaseName(node.id) || node.init?.type !== 'ArrowFunctionExpression') {
|
|
return
|
|
}
|
|
|
|
const declaration = node.parent
|
|
|
|
if (declaration.type === 'VariableDeclaration' && declaration.kind !== 'const') {
|
|
context.report({ node: node.id, messageId: 'useConst' })
|
|
}
|
|
|
|
const body = node.init.body
|
|
|
|
if (
|
|
body.type !== 'BlockStatement' ||
|
|
body.body.length !== 1 ||
|
|
body.body[0].type !== 'ReturnStatement' ||
|
|
!isJsx(body.body[0].argument)
|
|
) {
|
|
return
|
|
}
|
|
|
|
const returnedJsx = sourceCode.getText(body.body[0].argument)
|
|
const hasComments = sourceCode.getCommentsInside(body).length > 0
|
|
|
|
context.report({
|
|
node: body,
|
|
messageId: 'implicitReturn',
|
|
...(hasComments
|
|
? {}
|
|
: {
|
|
fix: (fixer) => fixer.replaceText(body, `(${returnedJsx})`),
|
|
}),
|
|
})
|
|
},
|
|
}
|
|
},
|
|
}
|
|
|
|
const isFunctionNode = (node) =>
|
|
node?.type === 'FunctionDeclaration' ||
|
|
node?.type === 'FunctionExpression' ||
|
|
node?.type === 'ArrowFunctionExpression'
|
|
|
|
const isAsyncFunctionNode = (node) => isFunctionNode(node) && node.async === true
|
|
|
|
const isUseServerDirectiveStatement = (statement) =>
|
|
statement?.type === 'ExpressionStatement' &&
|
|
statement.expression?.type === 'Literal' &&
|
|
statement.expression.value === 'use server'
|
|
|
|
const hasTopLevelUseServer = (program) =>
|
|
Array.isArray(program.body) && program.body.some(isUseServerDirectiveStatement)
|
|
|
|
const isTypeOnlyNamedExport = (node) => {
|
|
if (node.exportKind === 'type') return true
|
|
|
|
const declaration = node.declaration
|
|
if (!declaration) return false
|
|
|
|
return (
|
|
declaration.type === 'TSTypeAliasDeclaration' ||
|
|
declaration.type === 'TSInterfaceDeclaration' ||
|
|
declaration.type === 'TSModuleDeclaration'
|
|
)
|
|
}
|
|
|
|
const resolveLocalAsyncBinding = (program, name) => {
|
|
for (const statement of program.body) {
|
|
if (statement.type === 'FunctionDeclaration' && statement.id?.name === name) {
|
|
return isAsyncFunctionNode(statement)
|
|
}
|
|
|
|
if (statement.type === 'VariableDeclaration') {
|
|
for (const declarator of statement.declarations) {
|
|
if (declarator.id?.type === 'Identifier' && declarator.id.name === name) {
|
|
return isAsyncFunctionNode(declarator.init)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Next.js rejects any non-async-function export from a top-level `'use server'`
|
|
* module (https://nextjs.org/docs/messages/invalid-use-server-value). Inspired by
|
|
* eslint-plugin-next-recommended's export-server-actions-only, kept local because
|
|
* that package is unmaintained and ESLint-8-era.
|
|
*/
|
|
const useServerAsyncExports = {
|
|
meta: {
|
|
type: 'problem',
|
|
docs: {
|
|
description:
|
|
'Require top-level `use server` modules to export only async functions (plus type-only exports)',
|
|
},
|
|
schema: [],
|
|
messages: {
|
|
nonAsyncExport:
|
|
'A `"use server"` file can only export async functions (found non-async export). Move constants, types-as-values, and helpers to a separate module. See https://nextjs.org/docs/messages/invalid-use-server-value',
|
|
reexport:
|
|
'A `"use server"` file cannot re-export values (`export … from` / `export *`). Export async functions defined in this file only.',
|
|
},
|
|
},
|
|
create(context) {
|
|
let programNode = null
|
|
let useServer = false
|
|
|
|
const report = (node, messageId) => {
|
|
context.report({ node, messageId })
|
|
}
|
|
|
|
return {
|
|
Program(node) {
|
|
programNode = node
|
|
useServer = hasTopLevelUseServer(node)
|
|
},
|
|
|
|
ExportAllDeclaration(node) {
|
|
if (!useServer) return
|
|
if (node.exportKind === 'type') return
|
|
report(node, 'reexport')
|
|
},
|
|
|
|
ExportDefaultDeclaration(node) {
|
|
if (!useServer) return
|
|
|
|
const declaration = node.declaration
|
|
if (isAsyncFunctionNode(declaration)) return
|
|
|
|
if (declaration?.type === 'Identifier' && programNode) {
|
|
if (resolveLocalAsyncBinding(programNode, declaration.name)) return
|
|
}
|
|
|
|
report(node, 'nonAsyncExport')
|
|
},
|
|
|
|
ExportNamedDeclaration(node) {
|
|
if (!useServer) return
|
|
if (isTypeOnlyNamedExport(node)) return
|
|
|
|
if (node.source) {
|
|
const hasValueSpecifier =
|
|
!node.specifiers?.length ||
|
|
node.specifiers.some((specifier) => specifier.exportKind !== 'type')
|
|
if (hasValueSpecifier) {
|
|
report(node, 'reexport')
|
|
}
|
|
return
|
|
}
|
|
|
|
const declaration = node.declaration
|
|
|
|
if (declaration?.type === 'FunctionDeclaration') {
|
|
if (!isAsyncFunctionNode(declaration)) {
|
|
report(node, 'nonAsyncExport')
|
|
}
|
|
return
|
|
}
|
|
|
|
if (declaration?.type === 'VariableDeclaration') {
|
|
for (const declarator of declaration.declarations) {
|
|
if (!isAsyncFunctionNode(declarator.init)) {
|
|
report(node, 'nonAsyncExport')
|
|
return
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
if (declaration?.type === 'TSEnumDeclaration' || declaration?.type === 'ClassDeclaration') {
|
|
report(node, 'nonAsyncExport')
|
|
return
|
|
}
|
|
|
|
if (declaration) {
|
|
// Unrecognized value declaration (e.g. const enum already covered).
|
|
if (
|
|
declaration.type !== 'TSTypeAliasDeclaration' &&
|
|
declaration.type !== 'TSInterfaceDeclaration'
|
|
) {
|
|
report(node, 'nonAsyncExport')
|
|
}
|
|
return
|
|
}
|
|
|
|
for (const specifier of node.specifiers ?? []) {
|
|
if (specifier.exportKind === 'type') continue
|
|
if (specifier.type !== 'ExportSpecifier') {
|
|
report(node, 'nonAsyncExport')
|
|
continue
|
|
}
|
|
|
|
const localName = specifier.local?.name
|
|
if (!localName || !programNode || !resolveLocalAsyncBinding(programNode, localName)) {
|
|
report(specifier, 'nonAsyncExport')
|
|
}
|
|
}
|
|
},
|
|
}
|
|
},
|
|
}
|
|
|
|
export const localRules = {
|
|
rules: {
|
|
'component-arrow-conventions': componentArrowConventions,
|
|
'framework-default-export': frameworkDefaultExport,
|
|
'no-component-default-export': noComponentDefaultExport,
|
|
'use-server-async-exports': useServerAsyncExports,
|
|
},
|
|
}
|