Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/eslint-config/src/configs/nuxt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,20 @@ export default function nuxt(options: NuxtESLintConfigOptions): Linter.Config[]
},
})

const filePages = [
...(dirs.pages?.map(pagesDir => join(pagesDir, `**/*.${GLOB_EXTS}`)) || []),
].sort()

if (filePages.length) {
configs.push({
name: 'nuxt/pages',
files: filePages,
rules: {
'nuxt/no-page-meta-runtime-values': 'error',
},
})
}

configs.push({
name: 'nuxt/nuxt-config',
files: [
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { rule as preferImportMetaRule } from './prefer-import-meta'
import { rule as nuxtConfigOrderKeysRule } from './nuxt-config-keys-order'
import { rule as noNuxtConfigTestKeyRule } from './no-nuxt-config-test-key'
import { rule as noPageMetaRuntimeValuesRule } from './no-page-meta-runtime-values/index'

export default {
'prefer-import-meta': preferImportMetaRule,
'nuxt-config-keys-order': nuxtConfigOrderKeysRule,
'no-nuxt-config-test-key': noNuxtConfigTestKeyRule,
'no-page-meta-runtime-values': noPageMetaRuntimeValuesRule,
}
159 changes: 159 additions & 0 deletions packages/eslint-plugin/src/rules/no-page-meta-runtime-values/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import type { TSESTree as Tree } from '@typescript-eslint/utils'
import { AST_NODE_TYPES } from '@typescript-eslint/types'
import { createRule } from '../utils'

type MessageIds = 'composableCall' | 'thisExpression' | 'awaitExpression'
type Options = []

/**
* Non-`use*` APIs from Vue/Nuxt that depend on the component
* setup context being active. Any `use*()` call is caught separately
* via the naming convention check.
*
* All of these will not work inside `definePageMeta` at the eager (top)
* level because the macro is extracted into a separate chunk that runs
* before component setup.
*/
const CONTEXT_APIS = new Set([
// Vue reactivity (require active effect scope)
'ref',
'shallowRef',
'customRef',
'computed',
'reactive',
'shallowReactive',
'readonly',
'shallowReadonly',
'toRef',
'toRefs',
'watch',
'watchEffect',
'watchPostEffect',
'watchSyncEffect',
'effectScope',
'onScopeDispose',
// Vue lifecycle hooks
'onBeforeMount',
'onMounted',
'onBeforeUpdate',
'onUpdated',
'onBeforeUnmount',
'onUnmounted',
'onActivated',
'onDeactivated',
'onErrorCaptured',
'onRenderTracked',
'onRenderTriggered',
'onServerPrefetch',
// Vue component context
'inject',
'getCurrentInstance',
])

/**
* Matches the Vue composable naming convention: `use` followed by
* an uppercase letter. This catches both Nuxt built-in composables
* (useRoute, useNuxtApp, …) and third-party ones (useI18n, useAuth, …)
* which all require an active setup/app context.
*/
const USE_COMPOSABLE_RE = /^use[A-Z]/

export const rule = createRule<MessageIds, Options>({
name: 'no-page-meta-runtime-values',
meta: {
type: 'problem',
docs: {
description: 'Disallow runtime context values inside `definePageMeta` at the eager level, which is extracted into a separate chunk at build time and runs before component setup',
},
schema: [],
messages: {
composableCall: '`definePageMeta()` is extracted at build time and runs before component setup. `{{ name }}()` requires a Nuxt/Vue runtime context that is not available here. Move it inside a `middleware` or `validate` function.',
thisExpression: '`definePageMeta()` is extracted at build time and runs before component setup. `this` is not available in the extracted context.',
awaitExpression: '`definePageMeta()` is extracted at build time. `await` is not supported inside `definePageMeta`.',
},
},
defaultOptions: [],
create(context) {
let definePageMetaNode: Tree.CallExpression | null = null
// Tracks depth of function boundaries inside definePageMeta.
// Eager level = 0 means we're directly in the definePageMeta argument.
// Inside inline functions (middleware, validate) depth >= 1 β€” those are fine.
let functionDepth = 0

function isAtEagerLevel() {
return definePageMetaNode !== null && functionDepth === 0
}

function enterFunction() {
if (definePageMetaNode)
functionDepth++
}

function exitFunction() {
if (definePageMetaNode)
functionDepth--
}

return {
// ---- definePageMeta boundary ----

CallExpression(node: Tree.CallExpression) {
if (
node.callee.type === AST_NODE_TYPES.Identifier
&& node.callee.name === 'definePageMeta'
&& !definePageMetaNode
) {
definePageMetaNode = node
}
// Report composable/context-dependent calls at the eager level
else if (
isAtEagerLevel()
&& node.callee.type === AST_NODE_TYPES.Identifier
&& (CONTEXT_APIS.has(node.callee.name) || USE_COMPOSABLE_RE.test(node.callee.name))
) {
context.report({
node,
messageId: 'composableCall',
data: { name: node.callee.name },
})
}
},

'CallExpression:exit'(node: Tree.CallExpression) {
if (node === definePageMetaNode) {
definePageMetaNode = null
functionDepth = 0
}
},

// ---- Track function scopes ----

ArrowFunctionExpression: enterFunction,

Check failure on line 131 in packages/eslint-plugin/src/rules/no-page-meta-runtime-values/index.ts

View workflow job for this annotation

GitHub Actions / ci (20, macos-latest)

Inconsistently quoted property 'ArrowFunctionExpression' found
'ArrowFunctionExpression:exit': exitFunction,
FunctionExpression: enterFunction,

Check failure on line 133 in packages/eslint-plugin/src/rules/no-page-meta-runtime-values/index.ts

View workflow job for this annotation

GitHub Actions / ci (20, macos-latest)

Inconsistently quoted property 'FunctionExpression' found
'FunctionExpression:exit': exitFunction,
FunctionDeclaration: enterFunction,

Check failure on line 135 in packages/eslint-plugin/src/rules/no-page-meta-runtime-values/index.ts

View workflow job for this annotation

GitHub Actions / ci (20, macos-latest)

Inconsistently quoted property 'FunctionDeclaration' found
'FunctionDeclaration:exit': exitFunction,

// ---- Report eager-level problems ----

ThisExpression(node: Tree.ThisExpression) {
if (!isAtEagerLevel())
return
context.report({
node,
messageId: 'thisExpression',
})
},

AwaitExpression(node: Tree.AwaitExpression) {
if (!isAtEagerLevel())
return
context.report({
node,
messageId: 'awaitExpression',
})
},
}
},
})
Loading
Loading